Eerste commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
@code {
|
||||
/// <summary>Stuurt iemand zonder geldige sessie naar het inlogscherm, met de gevraagde pagina erbij.</summary>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var terug = Uri.EscapeDataString(
|
||||
Navigatie.ToBaseRelativePath(Navigatie.Uri));
|
||||
Navigatie.NavigateTo($"/inloggen?terug={terug}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@* Expliciet, niet via de impliciete usings van de Web-SDK: zo compileert dit bestand ook in
|
||||
het losse controleproject en merk je een fout hier en niet pas op de server. *@
|
||||
@using Microsoft.AspNetCore.Hosting
|
||||
@inject IWebHostEnvironment Omgeving
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="app.css?v=@Stempel" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
@* Als iemand de site op zijn telefoon bij de startknoppen zet. *@
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
<body>
|
||||
<Routes />
|
||||
<div id="blazor-error-ui">
|
||||
Er ging iets mis. Ververs de pagina.
|
||||
<a href="" class="reload">Vernieuwen</a>
|
||||
<span class="dismiss">×</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
|
||||
@* Kleine stukjes javascript die bij een pagina horen. Ze staan hier en niet in die pagina's
|
||||
zelf: Blazor vervangt bij het navigeren alleen de HTML, en een <script> dat zo binnenkomt
|
||||
wordt niet uitgevoerd. In de kop staan ze er altijd. *@
|
||||
<script src="validatie.js?v=@Stempel"></script>
|
||||
<script src="adverteren.js?v=@Stempel"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@code {
|
||||
// Een stempel achter app.css, afgeleid van wanneer dat bestand voor het laatst is
|
||||
// gewijzigd. Zonder dit houdt de browser de oude stijlregels vast na een uitrol: je krijgt
|
||||
// dan de nieuwe pagina met de oude opmaak, wat eruitziet als een fout die er niet is.
|
||||
// De gebruiker merkt daar niets van - zodra het bestand verandert, verandert de naam mee
|
||||
// en haalt de browser hem vanzelf opnieuw op.
|
||||
private static string? _stempel;
|
||||
|
||||
private string Stempel => _stempel ??= Bepaal();
|
||||
|
||||
private string Bepaal()
|
||||
{
|
||||
try
|
||||
{
|
||||
var pad = Path.Combine(Omgeving.WebRootPath, "app.css");
|
||||
return File.Exists(pad)
|
||||
? File.GetLastWriteTimeUtc(pad).Ticks.ToString("x")
|
||||
: "1";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Lukt het niet, dan liever een pagina zonder stempel dan geen pagina.
|
||||
return "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="schil">
|
||||
<header class="kop">
|
||||
@* Het logo is zelf het woordmerk, dus staat de naam er niet nog een keer naast. *@
|
||||
<a class="merk" href="/"><img src="/logo.png" alt="MijnBesluit" /></a>
|
||||
<div class="rechts">
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
@* De naam is meteen de ingang naar je eigen profiel: dat is de enige
|
||||
persoon die niet bij Mijn mensen staat. *@
|
||||
<a href="/profiel" class="zacht" title="Mijn profiel">
|
||||
@(context.User.Naam() is { Length: > 0 } naam ? naam : "Mijn profiel")
|
||||
</a>
|
||||
<form method="post" action="/uitloggen" style="margin:0">
|
||||
<AntiforgeryToken />
|
||||
<button type="submit" class="knop rustig" style="min-height:36px;padding:.3rem .8rem">Uitloggen</button>
|
||||
</form>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a href="/inloggen">Inloggen</a>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<AuthorizeView Context="menu">
|
||||
<Authorized>
|
||||
<nav class="nav">
|
||||
<NavLink href="/" Match="NavLinkMatch.All" ActiveClass="actief"><span class="teken">☉</span> Overzicht</NavLink>
|
||||
<NavLink href="/familie" ActiveClass="actief"><span class="teken">👥</span> Mensen</NavLink>
|
||||
<NavLink href="/kluis" ActiveClass="actief"><span class="teken">🔒</span> Kluis</NavLink>
|
||||
<NavLink href="/momenten" ActiveClass="actief"><span class="teken">🎬</span> Momenten</NavLink>
|
||||
<NavLink href="/nalatenschap" ActiveClass="actief"><span class="teken">🌿</span> Nalatenschap</NavLink>
|
||||
|
||||
@* Alleen zichtbaar voor een beheerder. Wie het niet is, ziet hier niets en krijgt
|
||||
op /beheer "Niet gevonden" - een pagina waarvan je niet weet dat hij bestaat,
|
||||
ga je ook niet proberen. Zie docs/Identiteit.md.
|
||||
|
||||
Dit komt uit het inlogcookie en NIET uit de database. De layout staat op elke
|
||||
pagina en wordt tegelijk met die pagina opgebouwd; een databasevraag hier liep
|
||||
daardoor gelijk op met de vraag van de pagina zelf, over dezelfde DbContext.
|
||||
Die kan maar een ding tegelijk, en het gevolg was een foutpagina die er soms
|
||||
wel en soms niet was ("A second operation was started on this context
|
||||
instance"). Een layout hoort niets op te halen. *@
|
||||
@if (menu.User.IsBeheerder())
|
||||
{
|
||||
<NavLink href="/beheer" ActiveClass="actief"><span class="teken">⚙</span> Beheer</NavLink>
|
||||
}
|
||||
</nav>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<main class="inhoud">
|
||||
@Body
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
@* De navigatie zit in MainLayout; dit bestand blijft leeg zodat de template-verwijzing niet breekt. *@
|
||||
@@ -0,0 +1,126 @@
|
||||
@* Foto's, documenten en opnamen bij een kluisitem, een persoon of een bericht.
|
||||
|
||||
Eén component voor alle drie, net als in de app, zodat ze er overal hetzelfde uitzien. Wat
|
||||
er per plek verschilt zijn de knoppen die je meegeeft: bij een persoon kun je een foto
|
||||
aanwijzen als profielfoto, bij een kluisitem niet.
|
||||
|
||||
Het kiezen gaat via InputFile en niet via een eigen knop: dan doet de browser het
|
||||
bestandsvenster, inclusief "meerdere tegelijk". *@
|
||||
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using MijnBesluit.Domain.Entities
|
||||
|
||||
<div class="veld">
|
||||
<label>@Titel</label>
|
||||
|
||||
@if (Bijlagen.Count > 0)
|
||||
{
|
||||
<div class="stapel" style="margin-bottom:.6rem">
|
||||
@foreach (var bijlage in Bijlagen)
|
||||
{
|
||||
<div class="rij bijlage">
|
||||
@* Een klein voorbeeld in de rij zelf. Eerder stond de foto op ware grootte
|
||||
onder elke regel; in een wijzigpopup duwde dat de rest van het formulier
|
||||
buiten beeld. Klikken opent hem groot, in een eigen tabblad. *@
|
||||
@if (IsAfbeelding(bijlage))
|
||||
{
|
||||
<a href="/bestand/@bijlage.Id" target="_blank" title="Groot bekijken">
|
||||
<img class="bijlage-klein" src="/bestand/@bijlage.Id/klein" alt="@bijlage.FileName" />
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="teken">@Teken(bijlage)</span>
|
||||
}
|
||||
|
||||
<div class="groei">
|
||||
<a href="/bestand/@bijlage.Id" target="_blank">@bijlage.FileName</a>
|
||||
<div class="zacht">
|
||||
@Grootte(bijlage.SizeBytes)
|
||||
@if (bijlage.IsProfilePhoto) { <text> · profielfoto</text> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (KanProfielfoto && IsAfbeelding(bijlage) && !bijlage.IsProfilePhoto)
|
||||
{
|
||||
<button type="button" class="knop rustig klein" disabled="@Bezig"
|
||||
title="Als profielfoto gebruiken"
|
||||
@onclick="() => OpProfielfoto.InvokeAsync(bijlage.Id)">★</button>
|
||||
}
|
||||
|
||||
<button type="button" class="knop rustig klein" disabled="@Bezig"
|
||||
title="Verwijderen"
|
||||
@onclick="() => Weghalen = bijlage.Id">🗑</button>
|
||||
</div>
|
||||
|
||||
@* Verwijderen is onomkeerbaar - de bijlage is daarna echt weg - dus eerst
|
||||
bevestigen, net als overal elders waar iets wordt weggehaald. *@
|
||||
@if (Weghalen == bijlage.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin:0 0 .6rem">
|
||||
<strong>@bijlage.FileName</strong> verwijderen?
|
||||
<div class="rij" style="margin-top:.5rem">
|
||||
<button type="button" class="knop gevaar" disabled="@Bezig"
|
||||
@onclick="() => VerwijderBevestigdAsync(bijlage.Id)">Ja, verwijderen</button>
|
||||
<button type="button" class="knop rustig" disabled="@Bezig"
|
||||
@onclick="() => Weghalen = null">Nee, laat staan</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<InputFile OnChange="OpKiezen" disabled="@Bezig" multiple
|
||||
accept="image/*,video/*,application/pdf,.doc,.docx,.txt" />
|
||||
|
||||
@if (Bezig)
|
||||
{
|
||||
<div class="zacht" style="margin-top:.4rem">Bezig met opslaan…</div>
|
||||
}
|
||||
|
||||
@if (Uitleg is not null)
|
||||
{
|
||||
<div class="uitleg">@Uitleg</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Titel { get; set; } = "Foto's en documenten";
|
||||
[Parameter] public string? Uitleg { get; set; }
|
||||
|
||||
[Parameter] public IReadOnlyList<VaultFile> Bijlagen { get; set; } = Array.Empty<VaultFile>();
|
||||
|
||||
/// <summary>Er loopt een overdracht; dan gaan de knoppen op slot.</summary>
|
||||
[Parameter] public bool Bezig { get; set; }
|
||||
|
||||
/// <summary>Mag hier een foto worden aangewezen als profielfoto? Alleen bij een persoon.</summary>
|
||||
[Parameter] public bool KanProfielfoto { get; set; }
|
||||
|
||||
[Parameter] public EventCallback<InputFileChangeEventArgs> OpKiezen { get; set; }
|
||||
[Parameter] public EventCallback<long> OpVerwijderen { get; set; }
|
||||
[Parameter] public EventCallback<long> OpProfielfoto { get; set; }
|
||||
|
||||
/// <summary>Het id van de bijlage die op een "weet je het zeker?" staat te wachten.</summary>
|
||||
private long? Weghalen;
|
||||
|
||||
private async Task VerwijderBevestigdAsync(long bijlageId)
|
||||
{
|
||||
Weghalen = null;
|
||||
await OpVerwijderen.InvokeAsync(bijlageId);
|
||||
}
|
||||
|
||||
private static bool IsAfbeelding(VaultFile f) =>
|
||||
f.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsVideo(VaultFile f) =>
|
||||
f.ContentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string Teken(VaultFile f) =>
|
||||
IsAfbeelding(f) ? "\U0001F4F7" : IsVideo(f) ? "\U0001F3AC" : "\U0001F4C4";
|
||||
|
||||
/// <summary>De grootte zoals je hem aan iemand laat zien: "1,4 MB".</summary>
|
||||
private static string Grootte(long bytes) => bytes >= 1024 * 1024
|
||||
? $"{bytes / 1024d / 1024d:0.#} MB"
|
||||
: $"{Math.Max(1, bytes / 1024)} kB";
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
@* Een kleine lijngrafiek van de groei per dag: nieuwe profielen en nieuwe kluisitems.
|
||||
|
||||
Puur SVG, geen library: het gaat om twee lijntjes over een dag of dertig, en dat is geen
|
||||
reden om een grafiekbibliotheek naar de beheerpagina te trekken. Alleen op de website, en
|
||||
alleen voor een beheerder - dit hoeft niet met de app mee. *@
|
||||
|
||||
<div class="grafiek">
|
||||
@if (Punten.Count < 2)
|
||||
{
|
||||
<p class="zacht" style="margin:0">Nog niet genoeg dagen om een lijn te tekenen.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg viewBox="0 0 @Breedte @Hoogte" preserveAspectRatio="none" class="grafieksvg"
|
||||
role="img" aria-label="Groei van de laatste @Punten.Count dagen">
|
||||
<line x1="0" y1="@(Hoogte - Marge)" x2="@Breedte" y2="@(Hoogte - Marge)" class="grafiek-as" />
|
||||
|
||||
<polyline points="@Lijn(p => p.NieuweKluisItems)" class="grafiek-lijn grafiek-lijn-kluis" />
|
||||
<polyline points="@Lijn(p => p.NieuweProfielen)" class="grafiek-lijn grafiek-lijn-profielen" />
|
||||
</svg>
|
||||
|
||||
<div class="grafiek-onderschrift">
|
||||
<span class="zacht">@Punten[0].Datum.ToString("d MMM")</span>
|
||||
<span class="zacht">@Punten[^1].Datum.ToString("d MMM")</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="grafiek-legenda">
|
||||
<span class="grafiek-legendapunt grafiek-lijn-profielen"></span>
|
||||
<span class="zacht">Nieuwe profielen (totaal @Punten.Sum(p => p.NieuweProfielen))</span>
|
||||
<span class="grafiek-legendapunt grafiek-lijn-kluis" style="margin-left:1.2rem"></span>
|
||||
<span class="zacht">Nieuwe kluisitems (totaal @Punten.Sum(p => p.NieuweKluisItems))</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public IReadOnlyList<GroeiPunt> Punten { get; set; } = Array.Empty<GroeiPunt>();
|
||||
|
||||
private const int Breedte = 600;
|
||||
private const int Hoogte = 160;
|
||||
private const int Marge = 8;
|
||||
|
||||
/// <summary>De hoogste dagwaarde over beide reeksen, zodat ze op dezelfde schaal staan. Nooit
|
||||
/// nul: bij een lege of vlakke grafiek blijft de lijn dan gewoon plat onderin staan.</summary>
|
||||
private int Max => Punten.Count == 0
|
||||
? 1
|
||||
: Math.Max(1, Punten.Max(p => Math.Max(p.NieuweProfielen, p.NieuweKluisItems)));
|
||||
|
||||
private string Lijn(Func<GroeiPunt, int> waarde)
|
||||
{
|
||||
if (Punten.Count < 2) return "";
|
||||
|
||||
var stap = (double)Breedte / (Punten.Count - 1);
|
||||
var bruikbareHoogte = Hoogte - Marge * 2;
|
||||
|
||||
var punten = Punten.Select((p, i) =>
|
||||
{
|
||||
var x = i * stap;
|
||||
var y = Hoogte - Marge - (waarde(p) / (double)Max * bruikbareHoogte);
|
||||
return FormattableString.Invariant($"{x:0.##},{y:0.##}");
|
||||
});
|
||||
|
||||
return string.Join(" ", punten);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using MijnBesluit.Application.Models;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.Web.Components.Onderdelen;
|
||||
|
||||
/// <summary>
|
||||
/// Hoe een kluisitem er op het scherm uitziet: de naam van de categorie en de ingevulde velden
|
||||
/// op een rij.
|
||||
///
|
||||
/// Dit stond in Kluis.razor. Het staat hier omdat de nalatenschapspagina precies hetzelfde moet
|
||||
/// tonen: een beheerder die na een overlijden een polis openslaat hoort dezelfde velden te zien
|
||||
/// als de eigenaar zelf. Twee kopieën van deze lijst lopen vroeg of laat uit elkaar, en dan mist
|
||||
/// er ergens stilletjes een veld.
|
||||
/// </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)
|
||||
{
|
||||
if (p.Website is not null) yield return ("Website", p.Website);
|
||||
if (p.Username is not null) yield return ("Gebruikersnaam", p.Username);
|
||||
if (p.Secret is not null) yield return ("Wachtwoord", p.Secret);
|
||||
if (p.Pincode is not null) yield return ("Pincode", p.Pincode);
|
||||
if (p.DeviceName is not null) yield return ("Apparaat", p.DeviceName);
|
||||
if (p.RecoveryCodes is not null) yield return ("Herstelcodes", p.RecoveryCodes);
|
||||
if (p.Platform is not null) yield return ("Medium", p.Platform);
|
||||
if (p.PlatformAnders is not null) yield return ("Welk medium", p.PlatformAnders);
|
||||
if (p.Insurer is not null) yield return ("Verzekeraar", p.Insurer);
|
||||
if (p.PolicyNumber is not null) yield return ("Polisnummer", p.PolicyNumber);
|
||||
if (p.ContactPhone is not null) yield return ("Telefoon", p.ContactPhone);
|
||||
if (p.BurialOrCremation is not null) yield return ("Begraven of cremeren", p.BurialOrCremation);
|
||||
if (p.Location is not null) yield return ("Locatie", p.Location);
|
||||
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);
|
||||
if (p.Notes is not null) yield return ("Toelichting", p.Notes);
|
||||
}
|
||||
|
||||
public static string Naam(VaultCategory c) => c switch
|
||||
{
|
||||
VaultCategory.Uitvaartwens => "Uitvaartwens",
|
||||
VaultCategory.Donorcodicil => "Donorcodicil",
|
||||
VaultCategory.Polis => "Polis",
|
||||
VaultCategory.Wachtwoord => "Wachtwoord",
|
||||
VaultCategory.Apparaat => "Apparaat",
|
||||
VaultCategory.Pincode => "Pincode",
|
||||
VaultCategory.Document => "Document",
|
||||
VaultCategory.Notitie => "Notitie",
|
||||
VaultCategory.SocialMedia => "Sociale media",
|
||||
VaultCategory.Rijbewijs => "Rijbewijs",
|
||||
VaultCategory.Paspoort => "Paspoort",
|
||||
_ => "Overig"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@* Het ronde penningetje voor een persoon: zijn profielfoto, en anders zijn beginletters.
|
||||
|
||||
Eén component voor alle lijsten waar mensen in staan - Mijn mensen, je beheerders, de
|
||||
nalatenschappen die aan jou zijn toevertrouwd. Ze horen er hetzelfde uit te zien, en dat
|
||||
blijft alleen zo als het op één plek staat.
|
||||
|
||||
Meegeven doe je alleen een naam en waar het plaatje vandaan komt; hoe dat plaatje eruit
|
||||
ziet is aan het stijlblad. Zonder bron valt hij terug op de letters, even groot, zodat de
|
||||
lijst niet verspringt zodra er ergens een foto bij komt. *@
|
||||
|
||||
@if (!string.IsNullOrEmpty(Bron))
|
||||
{
|
||||
<img class="penning" src="@Bron" alt="@Naam" title="@Naam" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="penning" title="@Naam">@Beginletters(Naam)</span>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>De naam van de persoon; alleen gebruikt voor de letters en de tekstballon.</summary>
|
||||
[Parameter] public string Naam { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Waar het plaatje vandaan komt. Op de website een adres van de server, in de app de
|
||||
/// inhoud zelf. Leeg of null betekent: geen foto, dus de letters.
|
||||
/// </summary>
|
||||
[Parameter] public string? Bron { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// De eerste letter van de voornaam en van de achternaam. Bij één woord alleen de eerste,
|
||||
/// bij een lege naam een vraagteken - er moet altijd iets in de penning staan.
|
||||
/// </summary>
|
||||
public static string Beginletters(string? naam)
|
||||
{
|
||||
var delen = (naam ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (delen.Length == 0) return "?";
|
||||
return delen.Length == 1
|
||||
? delen[0][..1].ToUpperInvariant()
|
||||
: (delen[0][..1] + delen[^1][..1]).ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@* Een venster over het scherm heen, voor wijzigen en bevestigen.
|
||||
|
||||
Dezelfde opzet als in de app, zodat wijzigen en verwijderen op de site en op de telefoon
|
||||
hetzelfde aanvoelen. Bewust geen JavaScript-dialoog: die is niet vorm te geven en ziet er
|
||||
in elke browser anders uit. *@
|
||||
|
||||
<div class="popup-achtergrond" @onclick="Sluiten">
|
||||
<div class="popup" @onclick:stopPropagation="true">
|
||||
<h2>@Titel</h2>
|
||||
|
||||
@ChildContent
|
||||
|
||||
@if (Foutmelding is not null)
|
||||
{
|
||||
<div class="melding fout" style="margin-top:.8rem">@Foutmelding</div>
|
||||
}
|
||||
|
||||
<div class="rij popup-knoppen">
|
||||
<button type="button" class="knop @(Gevaarlijk ? "gevaar" : "")" disabled="@Bezig" @onclick="Bevestigen">
|
||||
@Bevestigtekst
|
||||
</button>
|
||||
@if (!AlleenSluiten)
|
||||
{
|
||||
<button type="button" class="knop rustig" disabled="@Bezig" @onclick="Sluiten">Annuleren</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Titel { get; set; } = "";
|
||||
[Parameter] public string Bevestigtekst { get; set; } = "Opslaan";
|
||||
|
||||
/// <summary>Kleurt de knop als waarschuwing; voor verwijderen.</summary>
|
||||
[Parameter] public bool Gevaarlijk { get; set; }
|
||||
|
||||
[Parameter] public bool Bezig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Er valt niets te bevestigen of af te breken, alleen dicht te doen. Dan is "Annuleren"
|
||||
/// naast "Sluiten" twee knoppen voor hetzelfde. Voor een popup die alleen iets laat zien.
|
||||
/// Staat net zo in de app-versie; die twee horen gelijk te blijven.
|
||||
/// </summary>
|
||||
[Parameter] public bool AlleenSluiten { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Waarom Bevestigen niet is gelukt (bijvoorbeeld een verplicht veld dat leeg is). Staat
|
||||
/// IN de popup, niet op de pagina erachter - die is aan het oog onttrokken zolang de popup
|
||||
/// openstaat, dus een melding die daar verschijnt ziet niemand.
|
||||
/// </summary>
|
||||
[Parameter] public string? Foutmelding { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
[Parameter] public EventCallback OpBevestigen { get; set; }
|
||||
[Parameter] public EventCallback OpSluiten { get; set; }
|
||||
|
||||
private Task Bevestigen() => OpBevestigen.InvokeAsync();
|
||||
private Task Sluiten() => OpSluiten.InvokeAsync();
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
@* De privacyverklaring zelf. Staat hier als onderdeel en niet in een pagina, omdat de app hem
|
||||
ook laat zien - daar in een popup vanaf het overzicht, hier als pagina op /privacy.
|
||||
|
||||
Dit bestand staat twee keer op schijf: de app en de site delen geen project. Een zelfcontrole
|
||||
vergelijkt ze letterlijk, want twee versies van je privacyverklaring is erger dan twee versies
|
||||
van een knop: het is het document waar iemand zich op beroept.
|
||||
|
||||
Verandert er iets aan wat er wordt opgeslagen, wie het kan zien of wat er naar buiten gaat, dan
|
||||
verandert deze tekst mee - en dan hoort de datum hieronder mee te veranderen. *@
|
||||
|
||||
<p class="zacht">Laatst bijgewerkt op 19 september 2026.</p>
|
||||
|
||||
<p>
|
||||
MijnBesluit is een digitale kluis. Je legt er vast wat er geregeld moet worden, en je zet
|
||||
berichten klaar voor de mensen om je heen. Dat zijn je meest persoonlijke gegevens, en
|
||||
daarom staat hier zo precies mogelijk wat ermee gebeurt.
|
||||
</p>
|
||||
|
||||
<h2>Wie is verantwoordelijk</h2>
|
||||
<p>
|
||||
MijnBesluit is een dienst van <strong>Kwik-Bit Automatisering B.V.</strong><br />
|
||||
Boulevard 1945 nr. 216, 7511 AG Enschede<br />
|
||||
KvK-nummer 97571792
|
||||
</p>
|
||||
<p>
|
||||
Vragen over je gegevens kun je stellen via
|
||||
<a href="mailto:privacy@@mijnbesluit.nl">privacy@@mijnbesluit.nl</a>.
|
||||
</p>
|
||||
|
||||
<h2>Wat we vastleggen</h2>
|
||||
<ul>
|
||||
<li><strong>Je e-mailadres.</strong> Daarmee log je in; we sturen er een code naartoe.</li>
|
||||
<li><strong>Je naam en geboortedatum</strong>, als je die invult.</li>
|
||||
<li><strong>Wat je in je kluis zet</strong>: titels, notities, wachtwoorden, polissen,
|
||||
wensen, en de bestanden die je toevoegt.</li>
|
||||
<li><strong>De mensen die je toevoegt</strong>: hun naam, en het e-mailadres dat je
|
||||
invult om ze uit te nodigen.</li>
|
||||
<li><strong>De berichten</strong> die je voor iemand klaarzet, en wanneer ze bezorgd zijn.</li>
|
||||
<li><strong>Een logboek</strong> van handelingen: wie wanneer inlogde, wat er werd
|
||||
gewijzigd, wanneer een bericht is bezorgd. Daar staat geen inhoud in.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Wat er versleuteld is</h2>
|
||||
<p>
|
||||
Alles wat in je kluis staat en alles wat in je berichten staat, is versleuteld met een
|
||||
sleutel die bij jouw account hoort. Ook de bestanden die je toevoegt: die staan
|
||||
versleuteld op schijf. Wie de database in handen krijgt zonder de sleutel, ziet er niets
|
||||
van.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Eén uitzondering, en die noemen we liever zelf:</strong> de kleine versie van een
|
||||
profielfoto - het rondje van veertig pixels in een lijst - is niet versleuteld. Dat is een
|
||||
afweging geweest: een profielfoto is juist bedoeld om aan anderen te laten zien, en hem
|
||||
als kluismateriaal behandelen zou betekenen dat we voor elke kijker sleutels moeten
|
||||
uitdelen en weer intrekken. De foto op ware grootte is wél versleuteld.
|
||||
</p>
|
||||
<p>
|
||||
Het verkeer tussen de app en onze server gaat over een versleutelde verbinding, net als
|
||||
de website.
|
||||
</p>
|
||||
|
||||
<h2>Wie kan wat zien</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Jij</strong> ziet je eigen kluis. Niemand anders, ook wij niet: wij kunnen je
|
||||
kluisitems niet lezen zonder jouw account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Mensen met wie je in een familie zit</strong> zien elkaars naam en
|
||||
profielfoto. Niet elkaars kluis.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Iemand die je hebt aangewezen als nalatenschapsbeheerder</strong> krijgt
|
||||
toegang tot wat je daarvoor hebt bestemd, en pas nadat je overlijden is bevestigd
|
||||
volgens de procedure die in de app staat.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Iemand met wie je een kluisitem alvast "bespreekt".</strong> Dat kies je zelf,
|
||||
per item: die persoon ziet het dan meteen, inclusief een eventuele foto of bijlage,
|
||||
zonder dat daar een overlijden voor nodig is. Zonder die keuze ziet niemand het.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Een beheerder van MijnBesluit</strong> kan een profiel opzoeken op volledig
|
||||
e-mailadres en een ten onrechte bevestigd overlijden terugdraaien. Hij kan géén kluis
|
||||
openen. Elke handeling van een beheerder komt in het logboek.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Wat er naar buiten gaat</h2>
|
||||
<p>
|
||||
Wij verkopen geen gegevens en er staan geen meetscripts of volgcookies op deze site.
|
||||
Er zijn twee partijen die noodzakelijkerwijs iets te zien krijgen:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Google (Firebase Cloud Messaging)</strong> bezorgt de meldingen op je
|
||||
telefoon. Daarvoor gaat een kenmerk van je toestel naar Google, met de titel en de
|
||||
korte tekst van de melding. De inhoud van een bericht of een kluisitem gaat daar
|
||||
nooit in mee.
|
||||
</li>
|
||||
<li>
|
||||
<strong>De verzender van onze e-mail</strong>, voor de inlogcodes en uitnodigingen.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Adverteerders krijgen niets over jou te zien. Er wordt per advertentie geteld hoe vaak
|
||||
hij is getoond en aangeklikt, per uur - meer niet. Geen profielen, geen kenmerken, geen
|
||||
bezoeker die door de applicatie heen te volgen is.
|
||||
</p>
|
||||
|
||||
<h2>Hoe lang we het bewaren</h2>
|
||||
<p>
|
||||
Zolang je account bestaat. Hef je het op, dan wordt het verwijderd - zie hieronder. Het
|
||||
logboek blijft staan, omdat je achteraf moet kunnen nagaan wat er is gebeurd; daar staat
|
||||
geen inhoud in. Van gegevens die bij onze administratie horen, zoals facturen voor
|
||||
adverteerders, houden we de wettelijke bewaartermijn aan.
|
||||
</p>
|
||||
|
||||
<h2>Je account opheffen</h2>
|
||||
<p>
|
||||
Dat kan zelf, op elk moment, en het hoeft niet via ons te lopen:
|
||||
<a href="/account-verwijderen">mijnbesluit.nl/account-verwijderen</a>. Dat adres werkt ook
|
||||
als je de app niet meer hebt. In de app staat dezelfde knop bij Mijn profiel.
|
||||
</p>
|
||||
<p>
|
||||
Wat er dan weggaat: je kluis, je bestanden, de berichten die nog klaarstonden, je lijst
|
||||
met mensen, en je account zelf. Wat blijft: berichten die al bezorgd zijn - die zijn van
|
||||
degene die ze heeft gekregen - en je naam in de lijst van mensen die jou hebben
|
||||
toegevoegd. Dat laatste is hun aantekening, niet jouw gegeven; alles wat er aan jou
|
||||
vastzat, gaat eraf.
|
||||
</p>
|
||||
|
||||
<h2>Je rechten</h2>
|
||||
<p>
|
||||
Je mag inzien wat we van je hebben, het laten corrigeren, en het laten verwijderen. Het
|
||||
meeste doe je zelf in de app of op de website. Lukt iets niet, mail dan
|
||||
<a href="mailto:privacy@@mijnbesluit.nl">privacy@@mijnbesluit.nl</a>. Ben je het oneens
|
||||
met hoe wij met je gegevens omgaan, dan kun je een klacht indienen bij de Autoriteit
|
||||
Persoonsgegevens.
|
||||
</p>
|
||||
|
||||
<h2>Als er iets misgaat</h2>
|
||||
<p>
|
||||
Bij een datalek melden wij dat bij de Autoriteit Persoonsgegevens, en bij jou als het
|
||||
jouw gegevens raakt.
|
||||
</p>
|
||||
@@ -0,0 +1,95 @@
|
||||
@* De uitleg over MijnBesluit, zonder kop en zonder knoppen eromheen.
|
||||
|
||||
Dit staat als los onderdeel zodat de website en de app dezelfde tekst tonen: op de website als
|
||||
pagina met knoppen om aan te melden, in de app als popup vanaf het overzicht. De app en de site
|
||||
delen geen project, dus dit bestand staat twee keer op schijf; een zelfcontrole vergelijkt ze
|
||||
letterlijk, zodat ze niet uit elkaar gaan lopen.
|
||||
|
||||
Kop en knoppen staan er bewust niet in: op de website hoort er "Een account aanmaken" onder, en
|
||||
in de app ben je al aangemeld. *@
|
||||
|
||||
<p>
|
||||
Er zit van alles in je hoofd waar niemand anders bij kan. Het wachtwoord van je e-mail,
|
||||
waar de polissen liggen, hoe je je uitvaart voor je ziet, en de dingen die je nog tegen
|
||||
iemand had willen zeggen. Zolang alles goed gaat maakt dat niet uit. Gaat het niet goed,
|
||||
dan staan de mensen die van je houden met lege handen — op het slechtst denkbare moment.
|
||||
</p>
|
||||
<p>
|
||||
MijnBesluit is de plek waar dat wél ergens staat. Veilig opgeborgen zolang het van jou
|
||||
alleen is, en beschikbaar op het moment dat jij hebt bepaald.
|
||||
</p>
|
||||
|
||||
<h2>Vier dingen</h2>
|
||||
<ul style="padding-left:1.2rem;margin:0 0 1.5rem">
|
||||
<li style="margin-bottom:.7rem">
|
||||
<strong>Je kluis.</strong> Wachtwoorden voor websites en apparaten, de pincode van
|
||||
je telefoon, je sociale media, polissen, documenten, je uitvaartwensen. Alles gaat
|
||||
versleuteld de opslag in. Per onderdeel kies je zelf of het alleen voor jou is,
|
||||
pas na je overlijden vrijkomt, of op een datum die jij bepaalt. Je kunt een item ook
|
||||
meteen — nog gewoon terwijl je er bent — met je nalatenschapsbeheerder
|
||||
bespreken; die ziet het dan al, inclusief een eventuele foto of bijlage, zonder dat
|
||||
daar een overlijden voor nodig is.
|
||||
</li>
|
||||
<li style="margin-bottom:.7rem">
|
||||
<strong>Je mensen.</strong> De familie en vrienden die erbij horen. Je maakt hun
|
||||
profiel aan en nodigt ze uit. Tot ze die uitnodiging aannemen beheer jij dat
|
||||
profiel; daarna is het van hen en vullen ze hun eigen MijnBesluit in. Zo groeit
|
||||
het vanzelf verder.
|
||||
</li>
|
||||
<li style="margin-bottom:.7rem">
|
||||
<strong>Momenten.</strong> Een bericht of een video die je klaarzet voor iemand,
|
||||
te bezorgen op een datum die jij kiest — of pas als jij er niet meer bent.
|
||||
Een achttiende verjaardag, een huwelijk, of gewoon iets wat je nog wilde zeggen.
|
||||
En je kunt zelf ook berichten ontvangen die een ander voor jou heeft klaargezet.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Je nalatenschap.</strong> Wie er straks mag regelen wat er geregeld moet
|
||||
worden. Die persoon krijgt niet zomaar toegang: er moeten meerdere mensen
|
||||
onafhankelijk melden dat je bent overleden — waaronder iemand die jij zelf hebt
|
||||
aangewezen — en daarna heb jij nog een termijn om te laten weten dat je er gewoon
|
||||
nog bent. Pas als die voorbij is, gaat het open.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Hoe veilig is het?</h2>
|
||||
<p>
|
||||
Je legt hier je wachtwoorden en je laatste wensen vast. Dan moet je weten wat daarmee
|
||||
gebeurt.
|
||||
</p>
|
||||
<ul style="padding-left:1.2rem">
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Alles gaat versleuteld de opslag in.</strong> Elk onderdeel krijgt zijn eigen
|
||||
sleutel, en die sleutel wordt zelf ook weer versleuteld. Wie de database in handen
|
||||
zou krijgen, heeft daar niets aan: er staat geen leesbare regel in.
|
||||
</li>
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Bijlagen net zo.</strong> Een foto van je polis of een scan van je testament
|
||||
wordt versleuteld opgeslagen en pas ontsleuteld op het moment dat jij hem opent.
|
||||
</li>
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Er is geen wachtwoord.</strong> Je vult je e-mailadres in en krijgt een code
|
||||
die tien minuten geldig is en één keer werkt. Geen wachtwoord betekent: niets om te
|
||||
stelen, te raden of te hergebruiken. De code zelf staat ook versleuteld opgeslagen,
|
||||
dus zelfs wij kunnen hem niet teruglezen.
|
||||
</li>
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Niemand komt bij je kluis, ook wij niet.</strong> Wat je op "alleen voor
|
||||
mijzelf" zet, blijft dat. Je nalatenschapsbeheerder krijgt alleen wat jij hebt
|
||||
vrijgegeven, en pas op het moment dat jij hebt bepaald.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Er wordt bijgehouden wie wat doet.</strong> Elke keer dat er iets wordt
|
||||
geopend, gewijzigd of vrijgegeven, wordt dat vastgelegd. Zo is achteraf na te gaan
|
||||
wat er is gebeurd.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Volledige zekerheid bestaat niet, en dat beweren we ook niet. Wat we wel doen is elke
|
||||
keuze zo maken dat een fout of een inbraak zo min mogelijk oplevert.
|
||||
</p>
|
||||
|
||||
<h2>Wat kost het?</h2>
|
||||
<p style="margin-bottom:0">
|
||||
Een eigen MijnBesluit is gratis. Wat je vastlegt blijft van jou, en je kunt alles wat je
|
||||
hebt ingevoerd altijd zelf wijzigen of verwijderen.
|
||||
</p>
|
||||
@@ -0,0 +1,52 @@
|
||||
@* De versiehistorie: nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
||||
|
||||
Zelfde opzet als Productuitleg.razor en Privacytekst.razor: op de website een popup vanaf
|
||||
het overzicht, in de app hetzelfde. Dit bestand staat twee keer op schijf - de app en de site
|
||||
delen geen project - en een zelfcontrole vergelijkt ze letterlijk.
|
||||
|
||||
Wat hier NIET in hoort: bugfixes, tekstuele correcties, opmaak die is rechtgezet. Dit is voor
|
||||
"wat kan ik nu wat ik eerst niet kon", niet een logboek van elke wijziging in de code. *@
|
||||
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
||||
</p>
|
||||
|
||||
<h2 style="margin-top:0">20 september 2026</h2>
|
||||
<ul style="padding-left:1.2rem">
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Rijbewijs en paspoort in de kluis.</strong> Twee nieuwe categorieën, met een
|
||||
knop om meteen de voor- en achterkant (rijbewijs) of de foto-pagina (paspoort) te maken
|
||||
of te kiezen.
|
||||
</li>
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Mijn profiel als pop-up.</strong> Je eigen profiel bewerk je nu in dezelfde
|
||||
pop-up met vaste "Opslaan"-knop onderin als bij de andere mensen in je familie, in de
|
||||
app en op de website.
|
||||
</li>
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Duidelijker voor de nalatenschapsbeheerder.</strong> Bij "Aan mij toevertrouwd"
|
||||
zie je nu meteen hoeveel meldingen er nodig zijn en hoe lang de bezwaartermijn is.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Muziek en verdere wensen groeien mee.</strong> Die velden bij een kluisitem
|
||||
passen zich nu vanzelf aan de lengte van de tekst aan, ook op de website.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>19 september 2026</h2>
|
||||
<ul style="padding-left:1.2rem">
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Met mij gedeeld, ook in de app.</strong> Heeft iemand een kluisitem rechtstreeks
|
||||
met jou gedeeld om het vast te bespreken - bijvoorbeeld een uitvaartwens - dan zie je
|
||||
dat voortaan ook op je telefoon, niet meer alleen op de website.
|
||||
</li>
|
||||
<li style="margin-bottom:.5rem">
|
||||
<strong>Een gedeeld of nagelaten kluisitem bekijken.</strong> Dat opent nu als pop-up in
|
||||
dezelfde opmaak als het invulformulier, inclusief de foto's en documenten die erbij
|
||||
horen.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Met de vinger naar beneden trekken ververst het scherm.</strong> Werkt op Kluis,
|
||||
Mijn mensen, Momenten en Nalatenschap.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,238 @@
|
||||
@page "/account-verwijderen"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Authentication.Cookies
|
||||
@inject AccountService Accounts
|
||||
@inject AccountOpheffenService Opheffen
|
||||
@inject IHttpContextAccessor HttpContexten
|
||||
|
||||
@* Je account opheffen. Deze pagina doet twee dingen tegelijk, en dat is met opzet:
|
||||
|
||||
1. Het is het pad vanuit je eigen profiel, voor wie is ingelogd.
|
||||
2. Het is het openbare adres waar iemand die de app niet meer heeft alsnog kan laten
|
||||
verwijderen. Google Play eist dat apart, en terecht: een verwijderknop die alleen in de app
|
||||
zit, helpt niet als je de app al hebt weggegooid.
|
||||
|
||||
Statisch weergegeven, want er wordt een cookie gewist. Zie ook de inlogpagina: alle
|
||||
formulieren worden altijd gerenderd en alleen verborgen. *@
|
||||
|
||||
<PageTitle>Account opheffen - MijnBesluit</PageTitle>
|
||||
|
||||
<div class="middenblok">
|
||||
<h1>Je MijnBesluit opheffen</h1>
|
||||
|
||||
@if (Klaar)
|
||||
{
|
||||
<div class="melding goed">
|
||||
Je account is opgeheven. Je kluis, je bestanden en de berichten die nog klaarstonden
|
||||
zijn verwijderd.
|
||||
</div>
|
||||
<p>
|
||||
Wat al bezorgd was, blijft bij degene die het heeft gekregen. Je kunt op elk moment
|
||||
opnieuw beginnen met hetzelfde e-mailadres.
|
||||
</p>
|
||||
<p class="zacht"><a href="/">Terug naar MijnBesluit</a></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (Fout is not null) { <div class="melding fout">@Fout</div> }
|
||||
@if (Gelukt is not null) { <div class="melding goed">@Gelukt</div> }
|
||||
|
||||
<div hidden="@ToonCodeveld">
|
||||
<div class="kaart">
|
||||
<p style="margin-top:0"><strong>Dit verdwijnt:</strong></p>
|
||||
@if (Telling is not null)
|
||||
{
|
||||
<ul>
|
||||
<li>@Telling.Kluisitems in je kluis</li>
|
||||
<li>@Telling.Bestanden foto's en documenten</li>
|
||||
<li>@Telling.BerichtenKlaar berichten die nog klaarstonden</li>
|
||||
<li>je lijst met @Telling.Mensen mensen</li>
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul>
|
||||
<li>alles wat in je kluis staat</li>
|
||||
<li>je foto's en documenten</li>
|
||||
<li>de berichten die nog klaarstonden</li>
|
||||
<li>je lijst met mensen</li>
|
||||
</ul>
|
||||
}
|
||||
|
||||
<p><strong>Dit blijft:</strong></p>
|
||||
<ul>
|
||||
<li>
|
||||
berichten die al bezorgd zijn. Die zijn van degene die ze heeft gekregen,
|
||||
en die blijft ze ook kunnen lezen.
|
||||
</li>
|
||||
<li>
|
||||
je naam in de lijst van mensen die jou hebben toegevoegd. Dat is hun
|
||||
aantekening, niet jouw gegeven. Alles wat er aan jou vastzat - je
|
||||
e-mailadres, je geboortedatum, je foto - gaat eraf.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (Telling is { Nalatenschappen: > 0 })
|
||||
{
|
||||
<div class="melding let-op">
|
||||
Je bent aangewezen als nalatenschapsbeheerder bij @Telling.Nalatenschappen
|
||||
@(Telling.Nalatenschappen == 1 ? "persoon" : "mensen"). Die aanwijzing
|
||||
vervalt. Laat het ze even weten, anders rekenen ze op iemand die er niet
|
||||
meer is.
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="zacht">Er is geen prullenbak. Wij kunnen dit niet terughalen.</p>
|
||||
</div>
|
||||
|
||||
<EditForm Model="Aanvraag" FormName="opheffen-code" OnValidSubmit="CodeAanvragenAsync" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<div class="veld">
|
||||
<label for="email">E-mailadres van je account</label>
|
||||
<InputText id="email" @bind-Value="Aanvraag.Email" type="email"
|
||||
readonly="@Ingelogd" autocomplete="username" inputmode="email" />
|
||||
<ValidationMessage For="() => Aanvraag.Email" class="uitleg" />
|
||||
<div class="uitleg">
|
||||
We sturen er een code naartoe. Pas als je die invult gebeurt er iets,
|
||||
dus niemand kan jouw account opheffen zonder bij je e-mail te kunnen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="knop vol">Stuur me een code</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<div hidden="@(!ToonCodeveld)">
|
||||
<EditForm Model="Bevestig" FormName="opheffen-bevestigen" OnValidSubmit="VerwijderenAsync" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<div class="veld">
|
||||
<label for="email2">E-mailadres</label>
|
||||
<InputText id="email2" @bind-Value="Bevestig.Email" type="email" readonly />
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="code">De code uit die e-mail</label>
|
||||
<InputText id="code" @bind-Value="Bevestig.Code" class="codeveld" inputmode="numeric"
|
||||
autocomplete="one-time-code" maxlength="6" />
|
||||
<ValidationMessage For="() => Bevestig.Code" class="uitleg" />
|
||||
</div>
|
||||
|
||||
<div class="melding let-op">
|
||||
Hierna is het weg, en dat is niet terug te draaien.
|
||||
</div>
|
||||
|
||||
<button type="submit" class="knop gevaar">Mijn account definitief opheffen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<p class="zacht"><a href="/">Toch maar niet</a></p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm(FormName = "opheffen-code")] private AanvraagGegevens Aanvraag { get; set; } = new();
|
||||
[SupplyParameterFromForm(FormName = "opheffen-bevestigen")] private BevestigGegevens Bevestig { get; set; } = new();
|
||||
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private bool Ingelogd;
|
||||
private AccountOpheffenService.Telling? Telling;
|
||||
private bool _codeVerstuurd;
|
||||
private bool Klaar;
|
||||
private string? Fout, Gelukt;
|
||||
|
||||
private bool ToonCodeveld => _codeVerstuurd || !string.IsNullOrWhiteSpace(Bevestig.Email);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
|
||||
var gebruiker = (await AuthState).User;
|
||||
if (gebruiker.UserId() is not { } gebruikerId) return;
|
||||
|
||||
Ingelogd = true;
|
||||
|
||||
// Voor wie is ingelogd vullen we het adres in en laten we zien wát er weggaat. Wie hier
|
||||
// binnenkomt zonder in te loggen, krijgt hetzelfde verhaal maar zonder aantallen: die
|
||||
// zouden we alleen kunnen tonen door op een ingetypt adres te zoeken, en dan vertelt
|
||||
// deze pagina aan een vreemde hoeveel er in andermans kluis zit.
|
||||
if (string.IsNullOrWhiteSpace(Aanvraag.Email))
|
||||
Aanvraag.Email = gebruiker.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "";
|
||||
|
||||
var telling = await Opheffen.TelOpAsync(gebruikerId);
|
||||
if (telling.Succeeded) Telling = telling.Value;
|
||||
}
|
||||
|
||||
private async Task CodeAanvragenAsync()
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
|
||||
var resultaat = await Accounts.VraagOpheffencodeAanAsync(
|
||||
Aanvraag.Email, context.Connection.RemoteIpAddress?.ToString(), context.RequestAborted);
|
||||
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
Fout = resultaat.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_codeVerstuurd = true;
|
||||
Bevestig = new BevestigGegevens { Email = resultaat.Value!.Email };
|
||||
Gelukt = "We hebben je een code gestuurd. Vul hem hieronder in.";
|
||||
Fout = null;
|
||||
}
|
||||
|
||||
private async Task VerwijderenAsync()
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
|
||||
var controle = await Accounts.ControleerOpheffencodeAsync(
|
||||
Bevestig.Email, Bevestig.Code, context.RequestAborted);
|
||||
|
||||
if (!controle.Succeeded)
|
||||
{
|
||||
_codeVerstuurd = true;
|
||||
Fout = controle.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var weg = await Opheffen.VerwijderAsync(controle.Value!.Id, context.RequestAborted);
|
||||
if (!weg.Succeeded)
|
||||
{
|
||||
_codeVerstuurd = true;
|
||||
Fout = weg.Error;
|
||||
return;
|
||||
}
|
||||
|
||||
// Het cookie eraf. Zonder dit loopt de volgende pagina vast op een account dat niet meer
|
||||
// bestaat - en dat ziet eruit als een storing in plaats van als iets dat je zelf hebt
|
||||
// gedaan.
|
||||
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
Klaar = true;
|
||||
Fout = null;
|
||||
Gelukt = null;
|
||||
}
|
||||
|
||||
public class AanvraagGegevens
|
||||
{
|
||||
[Required(ErrorMessage = "Vul het e-mailadres van je account in.")]
|
||||
[EmailAddress(ErrorMessage = "Dit is geen geldig e-mailadres!")]
|
||||
public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
public class BevestigGegevens
|
||||
{
|
||||
[Required] public string Email { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Je moet de code invullen.")]
|
||||
public string Code { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
@page "/adverteren/campagnes"
|
||||
@using MijnBesluit.Domain.Entities
|
||||
@using MijnBesluit.Domain.Enums
|
||||
@inject AdvertentieService Adverteren
|
||||
@inject IHttpContextAccessor HttpContexten
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
@* Het eigen scherm van een adverteerder: zijn campagnes, de stand ervan, en wat er werkelijk is
|
||||
getoond.
|
||||
|
||||
Bewust statisch weergegeven en niet interactief. Wie hier is ingelogd, zit in een eigen cookie
|
||||
met een eigen schema (zie Adverteerderslogin) - en dat cookie lees je bij elk verzoek uit
|
||||
HttpContext. Bij een interactief scherm bestaat die HttpContext na de eerste render niet meer
|
||||
en zou het nummer van de adverteerder ergens moeten blijven staan. Dat is precies het soort
|
||||
ding dat je een keer verkeerd doet. *@
|
||||
|
||||
<PageTitle>Mijn advertenties - MijnBesluit</PageTitle>
|
||||
|
||||
<div>
|
||||
@if (Ik is null)
|
||||
{
|
||||
<h1>Adverteren</h1>
|
||||
<p>Je bent niet (meer) ingelogd. <a href="/adverteren">Log opnieuw in</a>.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="rij">
|
||||
<h1 class="groei">@Ik.Bedrijfsnaam</h1>
|
||||
<form method="post" action="/adverteren/uitloggen" style="margin:0">
|
||||
<AntiforgeryToken />
|
||||
<button type="submit" class="knop rustig">Uitloggen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (Melding is not null) { <div class="melding goed">@Melding</div> }
|
||||
|
||||
@if (Ik.IsGeblokkeerd)
|
||||
{
|
||||
<div class="melding fout">
|
||||
Er kunnen met dit account geen nieuwe advertenties worden ingediend. Neem contact
|
||||
met ons op als je denkt dat dat niet klopt.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p><a class="knop vol" href="/adverteren/nieuw">Nieuwe advertentie</a></p>
|
||||
}
|
||||
|
||||
@if (Campagnes.Count == 0)
|
||||
{
|
||||
<div class="kaart">
|
||||
<p>Je hebt nog geen advertenties. Met de knop hierboven stel je er een samen.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@foreach (var campagne in Campagnes)
|
||||
{
|
||||
var totaal = Totalen.TryGetValue(campagne.Id, out var t) ? t : (0, 0);
|
||||
var link = Adverteren.Betaallink(campagne);
|
||||
|
||||
<div class="kaart">
|
||||
<div class="rij">
|
||||
@if (campagne.HeeftAfbeelding)
|
||||
{
|
||||
<img src="/advertentie/@campagne.Id/afbeelding" alt="" class="advertentiebeeld" />
|
||||
}
|
||||
<div class="groei">
|
||||
<span class="groot">@campagne.Tekst</span>
|
||||
<div class="zacht">@campagne.Link</div>
|
||||
</div>
|
||||
<span class="label @Kleur(campagne.Stand)">@Standtekst(campagne)</span>
|
||||
</div>
|
||||
|
||||
<div class="zacht" style="margin-top:.6rem">
|
||||
@campagne.VanafDatum.ToString("dd-MM-yyyy") tot en met
|
||||
@campagne.TotEnMetDatum.ToString("dd-MM-yyyy") ·
|
||||
@campagne.PerUur keer per uur · @Euro(campagne.BedragCenten)
|
||||
</div>
|
||||
|
||||
@if (campagne.Stand == CampagneStand.WachtOpBetaling)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.8rem">
|
||||
<p style="margin:0 0 .6rem">
|
||||
Te betalen: <strong>@Euro(campagne.BedragCenten)</strong>, onder vermelding van
|
||||
<strong>@campagne.Betaalkenmerk</strong>.
|
||||
</p>
|
||||
@if (link is not null)
|
||||
{
|
||||
@* Een eigen tekst op de knop en niet het adres zelf: hier hoort te staan
|
||||
wát je doet en voor hoeveel, niet hoe de link is opgebouwd. *@
|
||||
<a class="knop vol" href="@link" target="_blank" rel="noopener noreferrer">
|
||||
Betaal @Euro(campagne.BedragCenten) via bunq
|
||||
</a>
|
||||
<div class="uitleg">
|
||||
Je betaalt bij bunq; die betaling komt niet vanzelf bij ons binnen.
|
||||
We vinken hem met de hand af, meestal dezelfde dag nog.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="uitleg">Neem contact met ons op voor de betaalgegevens.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (campagne.Stand == CampagneStand.Afgewezen && campagne.AfwijzingReden is { Length: > 0 } reden)
|
||||
{
|
||||
<div class="melding fout" style="margin-top:.8rem">Reden: @reden</div>
|
||||
}
|
||||
|
||||
@if (campagne.Stand is CampagneStand.Loopt or CampagneStand.Gestopt)
|
||||
{
|
||||
<div class="rij" style="margin-top:.8rem">
|
||||
<div class="kaart" style="margin:0;flex:1">
|
||||
<span class="groot">@totaal.Item1</span>
|
||||
<div class="zacht">keer getoond</div>
|
||||
</div>
|
||||
<div class="kaart" style="margin:0;flex:1">
|
||||
<span class="groot">@totaal.Item2</span>
|
||||
<div class="zacht">keer aangeklikt</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uitleg">
|
||||
Het geboekte aantal per uur is een maximum. Zijn er in een uur minder
|
||||
bezoekers dan je hebt geboekt, dan wordt de rest niet ingehaald.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="zacht"><a href="/adverteren">Uitleg over adverteren</a></p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromQuery(Name = "nieuw")] private long? Nieuw { get; set; }
|
||||
|
||||
private Adverteerder? Ik;
|
||||
private IReadOnlyList<Campagne> Campagnes = Array.Empty<Campagne>();
|
||||
private readonly Dictionary<long, (int, int)> Totalen = new();
|
||||
private string? Melding;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var context = HttpContexten.HttpContext;
|
||||
if (context is null) return;
|
||||
|
||||
// Het nummer komt uit het cookie van het adverteerdersschema, bij elk verzoek opnieuw.
|
||||
var id = await Adverteerderslogin.AdverteerderIdAsync(context);
|
||||
if (id is null) return;
|
||||
|
||||
Ik = await Adverteren.AdverteerderAsync(id.Value, context.RequestAborted);
|
||||
if (Ik is null) return;
|
||||
|
||||
Campagnes = await Adverteren.CampagnesAsync(Ik.Id, context.RequestAborted);
|
||||
|
||||
foreach (var campagne in Campagnes)
|
||||
Totalen[campagne.Id] = await Adverteren.TotalenAsync(campagne.Id, context.RequestAborted);
|
||||
|
||||
if (Nieuw is not null && Campagnes.Any(c => c.Id == Nieuw))
|
||||
Melding = "Je advertentie is ingediend. Hieronder staat wat er betaald moet worden.";
|
||||
}
|
||||
|
||||
// Eén plek die weet hoe een bedrag eruitziet; zie AdvertentieService.Euro.
|
||||
private static string Euro(int centen) => AdvertentieService.Euro(centen);
|
||||
|
||||
/// <summary>
|
||||
/// Wat er in het labeltje staat. Een goedgekeurde campagne waarvan de einddatum voorbij is
|
||||
/// heet hier "afgelopen" en niet "loopt": dat is geen aparte stand in de database - hij
|
||||
/// verschijnt vanzelf niet meer - maar wel wat de adverteerder ziet als hij kijkt.
|
||||
/// </summary>
|
||||
private static string Standtekst(Campagne campagne)
|
||||
{
|
||||
if (campagne.Stand == CampagneStand.Loopt &&
|
||||
campagne.TotEnMetDatum < DateOnly.FromDateTime(DateTime.Today))
|
||||
return "afgelopen";
|
||||
|
||||
return campagne.Stand switch
|
||||
{
|
||||
CampagneStand.Concept => "concept",
|
||||
CampagneStand.WachtOpBetaling => "wacht op betaling",
|
||||
CampagneStand.WachtOpGoedkeuring => "wacht op goedkeuring",
|
||||
CampagneStand.Loopt => "loopt",
|
||||
CampagneStand.Afgewezen => "afgewezen",
|
||||
CampagneStand.Gestopt => "gestopt",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private static string Kleur(CampagneStand stand) => stand switch
|
||||
{
|
||||
CampagneStand.Loopt => "goed",
|
||||
CampagneStand.Afgewezen => "fout",
|
||||
CampagneStand.Gestopt => "fout",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
@page "/adverteren/nieuw"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using MijnBesluit.Domain.Entities
|
||||
@inject AdvertentieService Adverteren
|
||||
@inject IHttpContextAccessor HttpContexten
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
@* Een advertentie samenstellen. Statisch weergegeven, net als het overzicht ernaast: het nummer
|
||||
van de adverteerder komt bij elk verzoek uit zijn eigen cookie.
|
||||
|
||||
De prijs wordt twee keer uitgerekend. Eén keer hieronder in de pagina zelf, zodat je ziet wat
|
||||
het kost terwijl je de datums verzet - dat is een gemak, verder niets. En één keer op de
|
||||
server bij het indienen; dát is het bedrag dat geldt. Wie met de pagina knoeit, verandert
|
||||
alleen wat hij zelf ziet staan. *@
|
||||
|
||||
<PageTitle>Nieuwe advertentie - MijnBesluit</PageTitle>
|
||||
|
||||
<div>
|
||||
<h1>Nieuwe advertentie</h1>
|
||||
|
||||
@if (Fout is not null) { <div class="melding fout">@Fout</div> }
|
||||
|
||||
<EditForm Model="Boeking" FormName="advertentie" OnValidSubmit="IndienenAsync" method="post"
|
||||
enctype="multipart/form-data">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="kaart">
|
||||
<div class="veld">
|
||||
<label for="tekst">De tekst</label>
|
||||
<InputTextArea id="tekst" @bind-Value="Boeking.Tekst" rows="3" maxlength="300" />
|
||||
<div class="uitleg">
|
||||
Eén of twee regels. Hoogstens 300 tekens, en wat er staat moet kloppen: we
|
||||
lezen hem voordat hij geplaatst wordt.
|
||||
</div>
|
||||
<ValidationMessage For="() => Boeking.Tekst" class="uitleg" />
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="link">De link</label>
|
||||
<InputText id="link" @bind-Value="Boeking.Link" type="url" placeholder="https://" />
|
||||
<div class="uitleg">Waar iemand terechtkomt die op de balk klikt.</div>
|
||||
<ValidationMessage For="() => Boeking.Link" class="uitleg" />
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="afbeelding">Logo of afbeelding</label>
|
||||
@* Een gewoon bestandsveld en geen InputFile: die hoort bij een interactief
|
||||
scherm. Dit formulier is een gewone POST, en de server pakt het bestand uit
|
||||
Request.Form.Files. *@
|
||||
<input id="afbeelding" name="afbeelding" type="file" accept="image/png,image/jpeg" />
|
||||
<div class="uitleg">
|
||||
Niet verplicht. We verkleinen hem zelf; in beeld is hij ongeveer 48 bij 48.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kaart">
|
||||
<div class="raster twee">
|
||||
@* oninput en niet @onchange: dit is een statische pagina, dus er is geen
|
||||
Blazor die meekijkt. Het zijn gewone HTML-kenmerken, en die overleven ook een
|
||||
pagina die via een link binnenkomt. De functie staat in adverteren.js. *@
|
||||
<div class="veld">
|
||||
<label for="vanaf">Vanaf</label>
|
||||
<InputDate id="vanaf" @bind-Value="Boeking.Vanaf" oninput="mbPrijs()" onchange="mbPrijs()" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="tot">Tot en met</label>
|
||||
<InputDate id="tot" @bind-Value="Boeking.TotEnMet" oninput="mbPrijs()" onchange="mbPrijs()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="peruur">Hoe vaak per uur</label>
|
||||
<InputNumber id="peruur" @bind-Value="Boeking.PerUur" min="1" max="@MaxPerUur"
|
||||
oninput="mbPrijs()" onchange="mbPrijs()" />
|
||||
<div class="uitleg">
|
||||
Hoogstens @MaxPerUur keer per uur. Dit is een maximum en geen belofte: zijn er
|
||||
in een uur minder bezoekers, dan wordt de rest niet ingehaald.
|
||||
</div>
|
||||
<ValidationMessage For="() => Boeking.PerUur" class="uitleg" />
|
||||
</div>
|
||||
|
||||
@* De tarieven staan als kenmerk op dit vak, zodat adverteren.js ze kan lezen zonder
|
||||
dat er bedragen in dat bestand staan. Wat daar wordt uitgerekend is een gemak, geen
|
||||
bedrag: bij het indienen rekent de server het zelf uit. *@
|
||||
<div class="melding" id="prijsvak"
|
||||
data-tarief="@Adverteren.Tarieven.TariefCenten"
|
||||
data-bodem="@Adverteren.Tarieven.MinimumCenten"
|
||||
data-max="@MaxPerUur">
|
||||
Dit kost <strong id="prijs">@Euro(Bedrag)</strong>
|
||||
<span class="zacht" id="prijssom">(@Dagen dagen × @Boeking.PerUur per uur × @Adverteren.Tarieven.TariefCenten cent,
|
||||
met een minimum van @Euro(Adverteren.Tarieven.MinimumCenten))</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="knop vol">Indienen</button>
|
||||
<div class="uitleg">
|
||||
Na het indienen krijg je een betaalverzoek. De advertentie gaat pas draaien nadat
|
||||
de betaling binnen is en we naar de inhoud hebben gekeken.
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
<p class="zacht"><a href="/adverteren/campagnes">Terug naar mijn advertenties</a></p>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm(FormName = "advertentie")] private Boekinggegevens Boeking { get; set; } = new();
|
||||
|
||||
private Adverteerder? Ik;
|
||||
private string? Fout;
|
||||
|
||||
private int MaxPerUur => Adverteren.Tarieven.MaxPerUur;
|
||||
private int Dagen => Math.Max(Boeking.TotEnMet.DayNumber - Boeking.Vanaf.DayNumber + 1, 0);
|
||||
private int Bedrag => Adverteren.BerekenBedrag(Boeking.Vanaf, Boeking.TotEnMet, Boeking.PerUur);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var context = HttpContexten.HttpContext;
|
||||
if (context is null) return;
|
||||
|
||||
if (await Adverteerderslogin.AdverteerderIdAsync(context) is not { } id)
|
||||
{
|
||||
Navigatie.NavigateTo("/adverteren", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
|
||||
Ik = await Adverteren.AdverteerderAsync(id, context.RequestAborted);
|
||||
}
|
||||
|
||||
private async Task IndienenAsync()
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
if (Ik is null) { Navigatie.NavigateTo("/adverteren", forceLoad: true); return; }
|
||||
|
||||
byte[]? afbeelding = null;
|
||||
string? soort = null;
|
||||
|
||||
var bestand = context.Request.Form.Files["afbeelding"];
|
||||
if (bestand is { Length: > 0 })
|
||||
{
|
||||
if (bestand.Length > Adverteren.Tarieven.MaxAfbeeldingBytes)
|
||||
{
|
||||
Fout = "De afbeelding is te groot.";
|
||||
return;
|
||||
}
|
||||
|
||||
using var geheugen = new MemoryStream();
|
||||
await bestand.CopyToAsync(geheugen, context.RequestAborted);
|
||||
afbeelding = geheugen.ToArray();
|
||||
soort = bestand.ContentType;
|
||||
}
|
||||
|
||||
var uitkomst = await Adverteren.DienInAsync(
|
||||
Ik.Id, Boeking.Tekst, Boeking.Link, Boeking.Vanaf, Boeking.TotEnMet, Boeking.PerUur,
|
||||
afbeelding, soort, context.RequestAborted);
|
||||
|
||||
if (!uitkomst.Succeeded)
|
||||
{
|
||||
Fout = uitkomst.Error;
|
||||
return;
|
||||
}
|
||||
|
||||
Navigatie.NavigateTo($"/adverteren/campagnes?nieuw={uitkomst.Value!.Id}", forceLoad: true);
|
||||
}
|
||||
|
||||
// Eén plek die weet hoe een bedrag eruitziet; zie AdvertentieService.Euro.
|
||||
private static string Euro(int centen) => AdvertentieService.Euro(centen);
|
||||
|
||||
public class Boekinggegevens
|
||||
{
|
||||
[Required(ErrorMessage = "Vul de tekst van de advertentie in.")]
|
||||
[StringLength(300, ErrorMessage = "De tekst mag hoogstens 300 tekens lang zijn.")]
|
||||
public string Tekst { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Vul de link in.")]
|
||||
public string Link { get; set; } = "";
|
||||
|
||||
public DateOnly Vanaf { get; set; } = DateOnly.FromDateTime(DateTime.Today).AddDays(1);
|
||||
public DateOnly TotEnMet { get; set; } = DateOnly.FromDateTime(DateTime.Today).AddDays(30);
|
||||
|
||||
[Range(1, 100, ErrorMessage = "Vul in hoe vaak de advertentie per uur mag worden getoond.")]
|
||||
public int PerUur { get; set; } = 5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
@page "/adverteren"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject AccountService Accounts
|
||||
@inject AdvertentieService Advertenties
|
||||
@inject IHttpContextAccessor HttpContexten
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
@* De voordeur van het adverteerdersgedeelte: wat het is, wat het kost, en inloggen.
|
||||
|
||||
Zelfde opzet als de inlogpagina: de pagina wordt statisch weergegeven, want alleen zo kan er
|
||||
een cookie worden gezet, en alle formulieren worden ALTIJD gerenderd en alleen verborgen met
|
||||
hidden. Bij een statische pagina is elke POST een nieuwe pagina-instantie; een formulier dat
|
||||
op dat moment niet gerenderd wordt, bestaat voor de server niet en levert
|
||||
"Cannot submit the form ... because no form on the page currently has that name" op. *@
|
||||
|
||||
<PageTitle>Adverteren op MijnBesluit</PageTitle>
|
||||
|
||||
<div>
|
||||
<h1>Adverteren op MijnBesluit</h1>
|
||||
|
||||
<p class="zacht">
|
||||
Eén balk boven het overzichtsscherm, met je logo, een regel tekst en een link naar je
|
||||
eigen website. Je boekt zelf een periode en hoe vaak je per uur in beeld komt.
|
||||
</p>
|
||||
|
||||
@if (Fout is not null) { <div class="melding fout">@Fout</div> }
|
||||
@if (Gelukt is not null) { <div class="melding goed">@Gelukt</div> }
|
||||
|
||||
<div hidden="@ToonCodeveld">
|
||||
<div class="kaart">
|
||||
<h2>Wat het kost</h2>
|
||||
<p>
|
||||
Het bedrag is <strong>dagen × vertoningen per uur × @Centen(Tarief)</strong>,
|
||||
met een minimum van @Euro(Bodem). Dertig dagen, vijf keer per uur komt dus uit op
|
||||
@Euro(Voorbeeld).
|
||||
</p>
|
||||
<p class="zacht">
|
||||
Dat aantal per uur is een maximum en geen belofte: zijn er in een uur minder
|
||||
bezoekers dan je hebt geboekt, dan wordt de rest niet ingehaald. In je eigen
|
||||
scherm zie je wat er werkelijk is getoond en aangeklikt.
|
||||
</p>
|
||||
|
||||
<details class="uitklap">
|
||||
<summary>Waar komt de advertentie te staan?</summary>
|
||||
<p>
|
||||
Boven de tegels op het overzichtsscherm, op de website en in de app, met het
|
||||
woord <em>Advertentie</em> erbij. Niet in de kluis, niet bij de berichten aan
|
||||
familie en nergens rond een overlijdensmelding. Dat is de afspraak met onze
|
||||
gebruikers, en daar wijken we niet van af.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details class="uitklap">
|
||||
<summary>Hoe gaat het verder?</summary>
|
||||
<p>
|
||||
Je stelt de advertentie samen, betaalt via een betaalverzoek, en wij kijken
|
||||
naar de inhoud. Pas na goedkeuring gaat hij draaien; je krijgt daar bericht
|
||||
van. Een advertentie die niet past weigeren we, met opgaaf van reden.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="raster twee">
|
||||
<EditForm Model="Aanvraag" FormName="adverteerder-code" OnValidSubmit="CodeAanvragenAsync" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<h2>Inloggen</h2>
|
||||
<div class="veld">
|
||||
<label for="email">E-mailadres</label>
|
||||
<InputText id="email" @bind-Value="Aanvraag.Email" type="email"
|
||||
autocomplete="username" inputmode="email" placeholder="alles in kleine letters" />
|
||||
<ValidationMessage For="() => Aanvraag.Email" class="uitleg" />
|
||||
<div class="uitleg">
|
||||
Je krijgt een code per e-mail. Geen wachtwoord om te onthouden.
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="knop vol">Code aanvragen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
<EditForm Model="Nieuw" FormName="adverteerder-nieuw" OnValidSubmit="AanmeldenAsync" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<h2>Nog geen account?</h2>
|
||||
<div class="veld">
|
||||
<label for="bedrijf">Bedrijfsnaam</label>
|
||||
<InputText id="bedrijf" @bind-Value="Nieuw.Bedrijfsnaam" autocomplete="organization" />
|
||||
<ValidationMessage For="() => Nieuw.Bedrijfsnaam" class="uitleg" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="contact">Contactpersoon</label>
|
||||
<InputText id="contact" @bind-Value="Nieuw.Contactpersoon" autocomplete="name" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="email3">E-mailadres</label>
|
||||
<InputText id="email3" @bind-Value="Nieuw.Email" type="email"
|
||||
autocomplete="email" inputmode="email" />
|
||||
<ValidationMessage For="() => Nieuw.Email" class="uitleg" />
|
||||
</div>
|
||||
<button type="submit" class="knop">Code aanvragen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="@(!ToonCodeveld)">
|
||||
<EditForm Model="Inlog" FormName="adverteerder-inloggen" OnValidSubmit="BinnenAsync" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<div class="veld">
|
||||
<label for="email2">E-mailadres</label>
|
||||
<InputText id="email2" @bind-Value="Inlog.Email" type="email" readonly />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="code">Code</label>
|
||||
<InputText id="code" @bind-Value="Inlog.Code" class="codeveld" inputmode="numeric"
|
||||
autocomplete="one-time-code" maxlength="6" />
|
||||
<div class="uitleg">
|
||||
De code is @((int)AccountService.CodeGeldigheid.TotalMinutes) minuten geldig.
|
||||
Geen e-mail gekregen? Kijk ook in je ongewenste post, of
|
||||
<a href="/adverteren">vraag een nieuwe aan</a>.
|
||||
</div>
|
||||
<ValidationMessage For="() => Inlog.Code" class="uitleg" />
|
||||
</div>
|
||||
<button type="submit" class="knop vol">Inloggen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<p class="zacht"><a href="/">Terug naar MijnBesluit</a></p>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm(FormName = "adverteerder-code")] private AanvraagGegevens Aanvraag { get; set; } = new();
|
||||
[SupplyParameterFromForm(FormName = "adverteerder-nieuw")] private NieuwGegevens Nieuw { get; set; } = new();
|
||||
[SupplyParameterFromForm(FormName = "adverteerder-inloggen")] private InlogGegevens Inlog { get; set; } = new();
|
||||
|
||||
private bool _codeVerstuurd;
|
||||
private string? Fout, Gelukt;
|
||||
|
||||
private int Tarief => Advertenties.Tarieven.TariefCenten;
|
||||
private int Bodem => Advertenties.Tarieven.MinimumCenten;
|
||||
private int Voorbeeld => Advertenties.BerekenBedrag(30, 5);
|
||||
|
||||
// Eén plek die weet hoe een bedrag eruitziet; zie AdvertentieService.Euro.
|
||||
private static string Euro(int centen) => AdvertentieService.Euro(centen);
|
||||
private static string Centen(int centen) => centen + " cent";
|
||||
|
||||
/// <summary>
|
||||
/// Staat er al een e-mailadres in het codeformulier, dan is die stap eerder bereikt en hoort
|
||||
/// het codeveld te staan - ook bij de eerste render van een POST, vóórdat de afhandeling
|
||||
/// heeft gedraaid.
|
||||
/// </summary>
|
||||
private bool ToonCodeveld => _codeVerstuurd || !string.IsNullOrWhiteSpace(Inlog.Email);
|
||||
|
||||
private Task CodeAanvragenAsync() => StuurCodeAsync(Aanvraag.Email, null, null);
|
||||
|
||||
private Task AanmeldenAsync() => StuurCodeAsync(Nieuw.Email, Nieuw.Bedrijfsnaam, Nieuw.Contactpersoon);
|
||||
|
||||
private async Task StuurCodeAsync(string email, string? bedrijfsnaam, string? contactpersoon)
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
|
||||
// De bedrijfsnaam gaat mee met de code en niet in dit scherm: zo kan niemand zich een
|
||||
// bedrijfsnaam aanmeten zonder eerst bij het e-mailadres te kunnen, en hoeft de pagina
|
||||
// niets te onthouden tussen twee formulieren door.
|
||||
var resultaat = await Accounts.VraagAdverteerderscodeAanAsync(
|
||||
email, bedrijfsnaam, contactpersoon,
|
||||
context.Connection.RemoteIpAddress?.ToString(), context.RequestAborted);
|
||||
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
Fout = resultaat.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_codeVerstuurd = true;
|
||||
Inlog = new InlogGegevens { Email = resultaat.Value!.Email };
|
||||
Gelukt = "We hebben je een code gestuurd. Vul hem hieronder in.";
|
||||
Fout = null;
|
||||
}
|
||||
|
||||
private async Task BinnenAsync()
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
|
||||
// Eerst vastpakken: verderop wordt het formulier leeggemaakt, en dan is dit adres weg.
|
||||
var adres = Inlog.Email;
|
||||
|
||||
var controle = await Accounts.ControleerAdverteerderscodeAsync(
|
||||
adres, Inlog.Code, context.RequestAborted);
|
||||
|
||||
if (!controle.Succeeded)
|
||||
{
|
||||
_codeVerstuurd = true;
|
||||
Fout = controle.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var adverteerder = await Advertenties.AdverteerderByEmailAsync(adres, context.RequestAborted);
|
||||
|
||||
if (adverteerder is null)
|
||||
{
|
||||
// De code klopte, maar er is nog geen account en er is ook geen bedrijfsnaam
|
||||
// meegegeven: dan heeft iemand het inlogformulier gebruikt terwijl hij nieuw is.
|
||||
if (string.IsNullOrWhiteSpace(controle.Value!.Bedrijfsnaam))
|
||||
{
|
||||
Fout = "We kennen dit e-mailadres nog niet. Vul hieronder je bedrijfsnaam in, " +
|
||||
"dan maken we een account aan.";
|
||||
Gelukt = null;
|
||||
Inlog = new InlogGegevens();
|
||||
Nieuw = new NieuwGegevens { Email = adres };
|
||||
return;
|
||||
}
|
||||
|
||||
var aangemaakt = await Advertenties.MeldAanAsync(
|
||||
controle.Value.Bedrijfsnaam, controle.Value.Contactpersoon, adres,
|
||||
context.RequestAborted);
|
||||
|
||||
if (!aangemaakt.Succeeded)
|
||||
{
|
||||
Fout = aangemaakt.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
adverteerder = aangemaakt.Value!;
|
||||
}
|
||||
|
||||
await Adverteerderslogin.MeldAanAsync(context, adverteerder);
|
||||
Navigatie.NavigateTo("/adverteren/campagnes", forceLoad: true);
|
||||
}
|
||||
|
||||
public class AanvraagGegevens
|
||||
{
|
||||
[Required(ErrorMessage = "Je moet een e-mailadres invullen!")]
|
||||
[EmailAddress(ErrorMessage = "Dit is geen geldig e-mailadres!")]
|
||||
public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
public class NieuwGegevens
|
||||
{
|
||||
[Required(ErrorMessage = "Vul de naam van je bedrijf in.")]
|
||||
public string Bedrijfsnaam { get; set; } = "";
|
||||
|
||||
public string Contactpersoon { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Je moet een e-mailadres invullen!")]
|
||||
[EmailAddress(ErrorMessage = "Dit is geen geldig e-mailadres!")]
|
||||
public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
public class InlogGegevens
|
||||
{
|
||||
[Required] public string Email { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Je moet een code invullen!")]
|
||||
public string Code { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
@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 ›</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 — 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)
|
||||
→ @samenvoeging.DoelNaam (@samenvoeging.DoelPersonId)
|
||||
</span>
|
||||
<div class="zacht">
|
||||
@Datums.DatumTijd(samenvoeging.SamengevoegdOp)
|
||||
· @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"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
@page "/beheer/adverteren"
|
||||
@attribute [Authorize]
|
||||
@inject BeheerService Beheerkant
|
||||
@inject AdvertentieService Adverteren
|
||||
@inject AdvertentietariefInstellingen TariefInstellingen
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Adverteren - Beheer</PageTitle>
|
||||
|
||||
@* Het beheerscherm voor advertenties. Links de adverteerders, rechts hun campagnes.
|
||||
|
||||
Twee handelingen per campagne, en dat zijn twee verschillende vragen:
|
||||
1. is het geld binnen (dat zie je in bunq, niet hier - er komt geen bericht terug);
|
||||
2. mag dit naast onze tekst over uitvaarten staan.
|
||||
|
||||
Zie docs/Adverteren.md. *@
|
||||
|
||||
@if (!Magdit)
|
||||
{
|
||||
@* Dezelfde melding als een pagina die niet bestaat. Wie geen beheerder is, hoort niet te
|
||||
weten dat hier iets te beheren valt. *@
|
||||
<h1>Niet gevonden</h1>
|
||||
<p class="zacht">Deze pagina bestaat niet, of hij is niet voor jou.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1>Adverteren</h1>
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
<div class="kaart" style="margin-bottom:1rem">
|
||||
<h2 style="margin-top:0">Tarief</h2>
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Het bedrag is dagen × vertoningen per uur × dit tarief per vertoning.
|
||||
</p>
|
||||
<div class="rij" style="align-items:flex-end;gap:.6rem;flex-wrap:wrap">
|
||||
<div class="veld" style="margin:0">
|
||||
<label for="tarief">Tarief (in centen)</label>
|
||||
<input id="tarief" type="number" min="1" style="width:8rem" @bind="NieuwTarief" />
|
||||
</div>
|
||||
<button class="knop rustig" disabled="@Bezig" @onclick="TariefOpslaanAsync">Opslaan</button>
|
||||
</div>
|
||||
<p class="zacht" style="margin:.5rem 0 0">
|
||||
Nu @HuidigTarief cent per vertoning per uur per dag. Dertig dagen en vijf keer per uur
|
||||
komt daarmee op @AdvertentieService.Euro(30 * 5 * HuidigTarief).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (Wachtend.Count > 0)
|
||||
{
|
||||
<div class="kaart" style="border-color:var(--accent)">
|
||||
<h2 style="margin-top:0">Er wacht iets op jou</h2>
|
||||
<div class="stapel">
|
||||
@foreach (var campagne in Wachtend)
|
||||
{
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<div class="groot">@campagne.Adverteerder?.Bedrijfsnaam</div>
|
||||
<div class="zacht">
|
||||
@Bedrag(campagne.BedragCenten) · kenmerk @campagne.Betaalkenmerk
|
||||
· @Standnaam(campagne.Stand)
|
||||
</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => Openen(campagne.AdverteerderId)">Bekijken</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="raster lijst" style="margin-top:1rem; align-items:start">
|
||||
|
||||
<div class="kaart">
|
||||
<h2 style="margin-top:0">Adverteerders</h2>
|
||||
|
||||
@if (Adverteerders.Count == 0)
|
||||
{
|
||||
<p class="zacht">Er is nog niemand aangemeld.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stapel">
|
||||
@foreach (var adverteerder in Adverteerders)
|
||||
{
|
||||
<button type="button"
|
||||
class="kaart compact klikbaar @(Gekozen?.Id == adverteerder.Id ? "zelf" : "")"
|
||||
style="width:100%;text-align:left"
|
||||
@onclick="() => Openen(adverteerder.Id)">
|
||||
<span class="groot">@adverteerder.Bedrijfsnaam</span>
|
||||
<div class="zacht">
|
||||
@adverteerder.Email
|
||||
@if (adverteerder.IsGeblokkeerd) { <text> · geblokkeerd</text> }
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="kaart">
|
||||
@if (Gekozen is null)
|
||||
{
|
||||
<h2 style="margin-top:0">Advertenties</h2>
|
||||
<p class="zacht">Kies links een adverteerder.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2 style="margin-top:0">@Gekozen.Bedrijfsnaam</h2>
|
||||
<p class="zacht" style="margin-top:0">
|
||||
@Gekozen.Email
|
||||
@if (!string.IsNullOrWhiteSpace(Gekozen.Contactpersoon)) { <text> · @Gekozen.Contactpersoon</text> }
|
||||
</p>
|
||||
|
||||
@if (Campagnes.Count == 0)
|
||||
{
|
||||
<p class="zacht">Deze adverteerder heeft nog geen advertenties.</p>
|
||||
}
|
||||
|
||||
<div class="stapel">
|
||||
@foreach (var campagne in Campagnes)
|
||||
{
|
||||
<div class="kaart compact">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span class="groot">@campagne.Tekst</span>
|
||||
<div class="zacht">
|
||||
@campagne.VanafDatum.ToString("dd-MM-yyyy") t/m
|
||||
@campagne.TotEnMetDatum.ToString("dd-MM-yyyy") ·
|
||||
@campagne.PerUur× per uur · @campagne.Dagen dagen
|
||||
</div>
|
||||
</div>
|
||||
<span class="label @Standkleur(campagne.Stand)">@Standnaam(campagne.Stand)</span>
|
||||
</div>
|
||||
|
||||
@if (campagne.HeeftAfbeelding)
|
||||
{
|
||||
<img src="/advertentie/@campagne.Id/afbeelding" alt=""
|
||||
style="max-width:100%;max-height:120px;border-radius:10px;margin-top:.6rem" />
|
||||
}
|
||||
|
||||
<div class="zacht" style="margin-top:.6rem">
|
||||
Gaat naar <a href="@campagne.Link" target="_blank" rel="noopener noreferrer nofollow">@campagne.Link</a>
|
||||
</div>
|
||||
|
||||
<div class="zacht" style="margin-top:.4rem">
|
||||
@Bedrag(campagne.BedragCenten) · kenmerk <strong>@campagne.Betaalkenmerk</strong>
|
||||
@if (campagne.BetaaldOp is { } betaald)
|
||||
{
|
||||
<text> · betaald op @betaald.ToLocalTime().ToString("dd-MM-yyyy")</text>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Getoond.TryGetValue(campagne.Id, out var tellers))
|
||||
{
|
||||
<div class="zacht" style="margin-top:.4rem">
|
||||
@tellers.Vertoningen getoond · @tellers.Kliks aangeklikt
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (campagne.AfwijzingReden is { Length: > 0 } reden)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.6rem">Afgewezen: @reden</div>
|
||||
}
|
||||
|
||||
@* De twee vinkjes. Goedkeuren kan pas als de betaling is afgevinkt;
|
||||
de knop staat er daarom niet eerder. *@
|
||||
<div class="rij" style="margin-top:.8rem">
|
||||
@if (!campagne.IsBetaald)
|
||||
{
|
||||
<button class="knop" disabled="@Bezig" @onclick="() => Betaald(campagne.Id)">
|
||||
Betaling gezien
|
||||
</button>
|
||||
}
|
||||
else if (campagne.Stand != CampagneStand.Loopt)
|
||||
{
|
||||
<button class="knop" disabled="@Bezig" @onclick="() => Goedkeuren(campagne.Id)">
|
||||
Goedkeuren
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="knop rustig" disabled="@Bezig" @onclick="() => Stopzetten(campagne.Id)">
|
||||
Stopzetten
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (campagne.Stand != CampagneStand.Afgewezen)
|
||||
{
|
||||
<button class="knop gevaar" disabled="@Bezig" @onclick="() => Afwijzen = campagne.Id">
|
||||
Afwijzen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Afwijzen == campagne.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.8rem">
|
||||
<div class="veld">
|
||||
<label for="reden-@campagne.Id">Waarom wijs je hem af?</label>
|
||||
<input id="reden-@campagne.Id" @bind="Reden"
|
||||
placeholder="Dit leest de adverteerder" />
|
||||
</div>
|
||||
<div class="rij">
|
||||
<button class="knop gevaar" disabled="@Bezig" @onclick="() => AfwijzenNu(campagne.Id)">
|
||||
Afwijzen en bericht sturen
|
||||
</button>
|
||||
<button class="knop rustig" disabled="@Bezig" @onclick="() => Afwijzen = null">
|
||||
Annuleren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private long _gebruikerId;
|
||||
private bool Magdit, Bezig;
|
||||
|
||||
private IReadOnlyList<Adverteerder> Adverteerders = Array.Empty<Adverteerder>();
|
||||
private IReadOnlyList<Campagne> Wachtend = Array.Empty<Campagne>();
|
||||
private IReadOnlyList<Campagne> Campagnes = Array.Empty<Campagne>();
|
||||
private Adverteerder? Gekozen;
|
||||
|
||||
/// <summary>Wat er per campagne werkelijk is getoond en aangeklikt.</summary>
|
||||
private Dictionary<long, (int Vertoningen, int Kliks)> Getoond = new();
|
||||
|
||||
private long? Afwijzen;
|
||||
private string Reden = "";
|
||||
private string? Melding, MeldingSoort;
|
||||
|
||||
private int HuidigTarief;
|
||||
private int NieuwTarief;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
_gebruikerId = (await AuthState).User.UserId() ?? 0;
|
||||
Magdit = await Beheerkant.IsBeheerderAsync(_gebruikerId);
|
||||
if (Magdit)
|
||||
{
|
||||
HuidigTarief = Adverteren.Tarieven.TariefCenten;
|
||||
NieuwTarief = HuidigTarief;
|
||||
await Verversen();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schrijft het tarief naar de instellingen. Deze pagina rekent zelf al met het oude tarief
|
||||
/// verder tot een herlaad; nieuwe boekingen elders pakken het nieuwe tarief op zodra
|
||||
/// ASP.NET Core het gewijzigde bestand heeft opgemerkt, meestal binnen enkele seconden.
|
||||
/// </summary>
|
||||
private async Task TariefOpslaanAsync()
|
||||
{
|
||||
Melding = null;
|
||||
|
||||
var probleem = AdvertentietariefInstellingen.Controleer(NieuwTarief);
|
||||
if (probleem is not null)
|
||||
{
|
||||
Melding = probleem;
|
||||
MeldingSoort = "fout";
|
||||
return;
|
||||
}
|
||||
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
TariefInstellingen.SchrijfTarief(NieuwTarief);
|
||||
HuidigTarief = NieuwTarief;
|
||||
Melding = "Tarief opgeslagen. Nieuwe boekingen rekenen hiermee zodra dit is opgepikt.";
|
||||
MeldingSoort = "goed";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melding = ex.Message;
|
||||
MeldingSoort = "fout";
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Verversen()
|
||||
{
|
||||
Adverteerders = await Adverteren.AdverteerdersAsync();
|
||||
Wachtend = await Adverteren.WachtOpBeheerderAsync();
|
||||
|
||||
if (Gekozen is not null)
|
||||
{
|
||||
Gekozen = await Adverteren.AdverteerderAsync(Gekozen.Id);
|
||||
Campagnes = Gekozen is null ? Array.Empty<Campagne>()
|
||||
: await Adverteren.CampagnesAsync(Gekozen.Id);
|
||||
|
||||
Getoond = new Dictionary<long, (int, int)>();
|
||||
foreach (var campagne in Campagnes)
|
||||
Getoond[campagne.Id] = await Adverteren.TotalenAsync(campagne.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Openen(long adverteerderId)
|
||||
{
|
||||
Afwijzen = null;
|
||||
Melding = null;
|
||||
Gekozen = await Adverteren.AdverteerderAsync(adverteerderId);
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private Task Betaald(long campagneId) =>
|
||||
DoenAsync(() => Adverteren.BetaaldAsync(campagneId, _gebruikerId), "De betaling staat genoteerd.");
|
||||
|
||||
private Task Goedkeuren(long campagneId) =>
|
||||
DoenAsync(() => Adverteren.GoedkeurenAsync(campagneId, _gebruikerId), "Goedgekeurd. Hij draait binnen zijn periode.");
|
||||
|
||||
private Task Stopzetten(long campagneId) =>
|
||||
DoenAsync(() => Adverteren.StopzettenAsync(campagneId, _gebruikerId), "Stopgezet.");
|
||||
|
||||
private async Task AfwijzenNu(long campagneId)
|
||||
{
|
||||
var reden = Reden;
|
||||
if (await DoenAsync(() => Adverteren.AfwijzenAsync(campagneId, _gebruikerId, reden), "Afgewezen; de adverteerder heeft bericht."))
|
||||
{
|
||||
Afwijzen = null;
|
||||
Reden = "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eén plek voor het patroon "doe iets, toon de uitkomst, ververs". Zo staat de afhandeling
|
||||
/// van fouten hier één keer en blijft er geen knop hangen als er iets misgaat.
|
||||
/// </summary>
|
||||
private async Task<bool> DoenAsync(Func<Task<Result>> werk, string gelukt)
|
||||
{
|
||||
Bezig = true;
|
||||
Melding = null;
|
||||
try
|
||||
{
|
||||
var uitkomst = await werk();
|
||||
Melding = uitkomst.Succeeded ? gelukt : uitkomst.Error;
|
||||
MeldingSoort = uitkomst.Succeeded ? "goed" : "fout";
|
||||
await Verversen();
|
||||
return uitkomst.Succeeded;
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private static string Bedrag(int centen) => AdvertentieService.Euro(centen);
|
||||
|
||||
private static string Standnaam(CampagneStand stand) => stand switch
|
||||
{
|
||||
CampagneStand.Concept => "Concept",
|
||||
CampagneStand.WachtOpBetaling => "Wacht op betaling",
|
||||
CampagneStand.WachtOpGoedkeuring => "Wacht op goedkeuring",
|
||||
CampagneStand.Loopt => "Loopt",
|
||||
CampagneStand.Afgewezen => "Afgewezen",
|
||||
_ => "Gestopt"
|
||||
};
|
||||
|
||||
private static string Standkleur(CampagneStand stand) => stand switch
|
||||
{
|
||||
CampagneStand.Loopt => "geclaimd",
|
||||
CampagneStand.WachtOpBetaling => "uitgenodigd",
|
||||
CampagneStand.WachtOpGoedkeuring => "uitgenodigd",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
@page "/familie"
|
||||
@attribute [Authorize]
|
||||
@using MijnBesluit.Application.Abstractions
|
||||
@inject FamilyService Families
|
||||
@inject IFamilyRepository FamilieOpslag
|
||||
@inject VaultService Kluis
|
||||
@inject IJSRuntime JS
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Mijn mensen - MijnBesluit</PageTitle>
|
||||
|
||||
@* Dezelfde indeling als in de app: een brede knop om iemand toe te voegen, daaronder
|
||||
compacte kaarten met naam en wat het van je is. Wijzigen en verwijderen gaan via een
|
||||
popup, zodat je op de kaart zelf alleen de drie knoppen ziet.
|
||||
|
||||
Wijzigen en verwijderen mag zolang het profiel niet geclaimd is - ook na een uitnodiging,
|
||||
want een tikfout in een e-mailadres merk je pas als de mail nergens aankomt. *@
|
||||
|
||||
<h1>Mijn mensen</h1>
|
||||
<p class="zacht">
|
||||
Je familie en je vrienden — iedereen die erbij hoort. Voeg ze toe en nodig ze uit.
|
||||
Zolang iemand de uitnodiging niet heeft aangenomen, beheer jij dat profiel. Daarna is het
|
||||
van hem of haar.
|
||||
</p>
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
@if (Leden is null)
|
||||
{
|
||||
<p class="zacht">Bezig met laden…</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="knop vol" style="margin-bottom:1rem" disabled="@Bezig" @onclick="Nieuw">
|
||||
Toevoegen persoon
|
||||
</button>
|
||||
|
||||
@* Jezelf bovenaan. Je staat niet in je eigen ledenlijst - je bent geen familielid van
|
||||
jezelf - maar dit is wel de plek waar mensen je zoeken als ze hun eigen gegevens willen
|
||||
wijzigen. De kaart doet zelf niets; hij brengt je naar Mijn profiel. *@
|
||||
<a class="kaart compact klikbaar zelf" href="/profiel" title="Mijn profiel">
|
||||
<div class="rij persoon">
|
||||
<Penning Naam="@_mijnNaam" Bron="@Foto(_persoonId)" />
|
||||
<div class="groei">
|
||||
<span class="groot" title="@_mijnNaam">@_mijnNaam</span>
|
||||
<div class="onderregel">
|
||||
<span class="zacht">Je eigen gegevens en foto</span>
|
||||
<span class="label geclaimd">Jijzelf</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="stapel">
|
||||
@foreach (var lid in Leden)
|
||||
{
|
||||
<div class="kaart compact">
|
||||
<div class="rij persoon">
|
||||
@* De profielfoto als die er is, anders de beginletters. Even groot, zodat de
|
||||
lijst niet verspringt zodra er ergens een foto bij komt. *@
|
||||
<Penning Naam="@lid.DisplayName" Bron="@Foto(lid.PersonId)" />
|
||||
<div class="groei">
|
||||
<span class="groot" title="@lid.DisplayName">@lid.DisplayName</span>
|
||||
|
||||
@* De status staat hier en niet naast de naam. Daar hield de naam op een
|
||||
telefoon te weinig ruimte over; op deze regel past hij wel, en de
|
||||
status staat nog steeds altijd rechts op de tegel. *@
|
||||
<div class="onderregel">
|
||||
<span class="zacht">@Onderregel(lid)</span>
|
||||
|
||||
@switch (lid.State)
|
||||
{
|
||||
case ProfileState.Geclaimd:
|
||||
<span class="label geclaimd" title="Deze persoon beheert het profiel zelf">Geclaimd</span>
|
||||
break;
|
||||
case ProfileState.Uitgenodigd:
|
||||
<span class="label uitgenodigd" title="De uitnodiging staat nog open">Uitgenodigd</span>
|
||||
break;
|
||||
default:
|
||||
<span class="label" title="Jij beheert dit profiel">Door jou</span>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* De prullenbak staat op ELKE tegel, ook bij een profiel dat iemand zelf beheert.
|
||||
Dat je Karin uit je lijst haalt zegt niets over haar eigen MijnBesluit: haar
|
||||
profiel blijft van haar, het verdwijnt alleen hier. Wijzigen en uitnodigen
|
||||
horen daar niet bij - dat kán niet meer zodra iemand heeft geclaimd.
|
||||
|
||||
Is het profiel geclaimd, dan staat "Geclaimd op ..." links op deze regel,
|
||||
naast Verwijderen - zo blijft de tegel net zo laag als de andere. *@
|
||||
<div class="rij" style="margin-top:.6rem">
|
||||
@if (lid.State == ProfileState.Geclaimd)
|
||||
{
|
||||
<span class="zacht groei">
|
||||
Geclaimd@(lid.ClaimedAt is { } sinds ? $" op {KortDatum(sinds)}" : "").
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (lid.State != ProfileState.Geclaimd)
|
||||
{
|
||||
<button class="knop rustig klein" disabled="@Bezig" @onclick="() => Wijzigen(lid)"
|
||||
title="Gegevens wijzigen">
|
||||
✎ Wijzigen
|
||||
</button>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(lid.Email))
|
||||
{
|
||||
<button class="knop rustig klein" disabled="@Bezig" @onclick="() => Uitnodigen(lid.PersonId)"
|
||||
title="@(lid.State == ProfileState.Uitgenodigd ? "Opnieuw uitnodigen" : "Uitnodiging sturen")">
|
||||
✉ @(lid.State == ProfileState.Uitgenodigd ? "Opnieuw" : "Uitnodigen")
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
<button class="knop rustig klein" disabled="@Bezig" @onclick="() => Weghalen = lid"
|
||||
title="Bij je mensen weghalen">
|
||||
🗑 Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lid.State != ProfileState.Geclaimd && string.IsNullOrWhiteSpace(lid.Email))
|
||||
{
|
||||
<div class="zacht" style="margin-top:.5rem">
|
||||
Vul een e-mailadres in om @(lid.DisplayName) te kunnen uitnodigen.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Leden.Count == 0)
|
||||
{
|
||||
<div class="leeg">
|
||||
<span class="teken">👥</span>
|
||||
Er staat hier nog niemand. Voeg je partner of je kinderen toe;
|
||||
zij krijgen een uitnodiging en kunnen daarna hun eigen MijnBesluit invullen.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@* ---------------------------------------------------------------- toevoegen en wijzigen *@
|
||||
@if (Bewerken is not null)
|
||||
{
|
||||
<Popup Titel="@(Bewerken.PersoonId == 0 ? "Iemand toevoegen" : "Gegevens wijzigen")"
|
||||
Bevestigtekst="Opslaan" Bezig="@Bezig"
|
||||
Foutmelding="@(MeldingSoort == "fout" ? Melding : null)"
|
||||
OpBevestigen="Opslaan" OpSluiten="Sluiten">
|
||||
|
||||
<div class="raster twee">
|
||||
<div class="veld"><label for="vn">Voornaam</label><input id="vn" @bind="Bewerken.Voornaam" /></div>
|
||||
<div class="veld"><label for="an">Achternaam</label><input id="an" @bind="Bewerken.Achternaam" /></div>
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="rel">Wat is het van je?</label>
|
||||
<select id="rel" @bind="Bewerken.Relatie">
|
||||
@foreach (var (waarde, naam) in MijnBesluit.Domain.Rules.Relaties.Keuzes)
|
||||
{
|
||||
<option value="@waarde">@naam</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="gb">Geboortedatum</label>
|
||||
@* Op de site gewoon een datumveld: de agendaknop van de browser werkt hier wel.
|
||||
In de app staat een keuzelijstje, want in een webview doet die knop niets. *@
|
||||
<input id="gb" type="date" max="@DateTime.Today.ToString("yyyy-MM-dd")"
|
||||
@bind="Bewerken.Geboortedatum" />
|
||||
<div class="uitleg">Niet verplicht. Met een datum staat je familie bovenaan op leeftijd.</div>
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="em">E-mailadres</label>
|
||||
<input id="em" type="email" @bind="Bewerken.Email" />
|
||||
<div class="uitleg">Met een adres kun je hem uitnodigen. Zonder adres beheer je het profiel zelf.</div>
|
||||
</div>
|
||||
|
||||
@if (Bewerken.PersoonId == 0)
|
||||
{
|
||||
<div class="veld">
|
||||
<label>Foto's en documenten</label>
|
||||
<div class="uitleg">
|
||||
Die kun je toevoegen zodra deze persoon is opgeslagen; ze horen bij hem en
|
||||
hij moet er eerst zijn.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Bijlagenblok Bijlagen="Bijlagen" Bezig="@Uploaden" KanProfielfoto="true"
|
||||
Uitleg="Een foto van hem samen met jou, een kopie van zijn paspoort, een brief. Met het sterretje maak je er zijn profielfoto van."
|
||||
OpKiezen="BijlageKiezen"
|
||||
OpVerwijderen="BijlageWeg"
|
||||
OpProfielfoto="Profielfoto" />
|
||||
}
|
||||
|
||||
@if (BewerkLid is { } lid)
|
||||
{
|
||||
<div class="zacht geschiedenis">
|
||||
<div>Profiel aangemaakt op @Datum(lid.CreatedAt)</div>
|
||||
|
||||
@if (lid.InvitedAt is { } verstuurd)
|
||||
{
|
||||
<div>
|
||||
Uitgenodigd op @Datum(verstuurd)@(lid.InvitationResends > 0
|
||||
? $" ({lid.InvitationResends + 1}× verstuurd)" : "")
|
||||
</div>
|
||||
|
||||
@if (lid.InvitationExpiresAt is { } tot)
|
||||
{
|
||||
<div>
|
||||
@(tot > DateTime.UtcNow
|
||||
? $"Uitnodiging geldig tot {Datum(tot)}"
|
||||
: $"Uitnodiging verlopen op {Datum(tot)}")
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>Nog niet uitgenodigd.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Popup>
|
||||
}
|
||||
|
||||
@* ---------------------------------------------------------------- verwijderen *@
|
||||
@if (Weghalen is not null)
|
||||
{
|
||||
@* Drie gevallen, zoals in de app.
|
||||
|
||||
1. Hij beheert je nalatenschap. Dan kan het niet, en dit scherm zegt waar je dat weghaalt.
|
||||
De server weigert het ook; dit is er zodat je niet eerst op "verwijderen" hoeft te
|
||||
drukken om erachter te komen.
|
||||
2. Er staan momenten voor hem klaar. Die verdwijnen mee, en dat is het soort ding dat je
|
||||
wilt weten vóór je op "ja" drukt - niet erna.
|
||||
3. Gewoon weghalen. *@
|
||||
@if (Weghalen.IsBeheerder)
|
||||
{
|
||||
<Popup Titel="@($"{Weghalen.DisplayName} kan niet weg")"
|
||||
Bevestigtekst="Begrepen" AlleenSluiten="true" Bezig="@Bezig"
|
||||
OpBevestigen="() => Weghalen = null" OpSluiten="() => Weghalen = null">
|
||||
|
||||
<p>
|
||||
@Weghalen.DisplayName is aangewezen om je nalatenschap te beheren. Zolang dat zo
|
||||
is, hoort hij bij je mensen te staan.
|
||||
</p>
|
||||
<p class="zacht">
|
||||
Haal hem eerst weg bij <strong>Nalatenschap</strong>. Dat is met opzet een aparte
|
||||
handeling: wie je nalatenschap beheert, is een besluit dat je niet per ongeluk
|
||||
terugdraait door iemand uit een lijst te verwijderen.
|
||||
</p>
|
||||
</Popup>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Popup Titel="@($"{Weghalen.DisplayName} verwijderen?")"
|
||||
Bevestigtekst="Verwijderen" Gevaarlijk="true" Bezig="@Bezig"
|
||||
OpBevestigen="() => Verwijderen(Weghalen.PersonId)" OpSluiten="() => Weghalen = null">
|
||||
|
||||
<p>
|
||||
@Weghalen.DisplayName verdwijnt uit je lijst. Een uitnodiging die nog openstaat
|
||||
vervalt daarmee.
|
||||
</p>
|
||||
|
||||
@if (Weghalen.MomentenKlaar > 0)
|
||||
{
|
||||
<div class="melding let-op">
|
||||
Er @(Weghalen.MomentenKlaar == 1 ? "staat nog 1 moment" : $"staan nog {Weghalen.MomentenKlaar} momenten")
|
||||
voor @Weghalen.DisplayName klaar. @(Weghalen.MomentenKlaar == 1 ? "Dat verdwijnt" : "Die verdwijnen") mee
|
||||
en @(Weghalen.MomentenKlaar == 1 ? "wordt" : "worden") dus nooit bezorgd.
|
||||
Wat al bezorgd is blijft staan.
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="zacht">
|
||||
@if (Weghalen.State == ProfileState.Geclaimd)
|
||||
{
|
||||
<text>
|
||||
@Weghalen.DisplayName beheert dit profiel zelf. Het verdwijnt alleen bij
|
||||
jou; zijn eigen MijnBesluit blijft zoals het is.
|
||||
</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>
|
||||
Het profiel zelf blijft bestaan; hij kan in andere families staan of zijn
|
||||
eigen MijnBesluit hebben.
|
||||
</text>
|
||||
}
|
||||
</p>
|
||||
</Popup>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private long _gebruikerId, _persoonId, _familieId;
|
||||
private string _mijnNaam = "Mijn profiel";
|
||||
private IReadOnlyList<FamilyMemberView>? Leden;
|
||||
private string? Melding, MeldingSoort;
|
||||
private bool Bezig;
|
||||
|
||||
/// <summary>Het formulier voor toevoegen en wijzigen; null als er niets openstaat.</summary>
|
||||
private Formulier? Bewerken;
|
||||
|
||||
/// <summary>Het lid dat bij <see cref="Bewerken"/> hoort; null bij toevoegen. Nodig omdat het
|
||||
/// formulier alleen de te wijzigen velden bevat, niet de geschiedenis van het profiel.</summary>
|
||||
private FamilyMemberView? BewerkLid;
|
||||
|
||||
/// <summary>Wie er op verwijderen staat te wachten.</summary>
|
||||
private FamilyMemberView? Weghalen;
|
||||
|
||||
/// <summary>De foto's en documenten bij de persoon die in het wijzigscherm staat.</summary>
|
||||
private IReadOnlyList<VaultFile> Bijlagen = Array.Empty<VaultFile>();
|
||||
private bool Uploaden;
|
||||
|
||||
/// <summary>Zoveel bestanden tegelijk kiezen mag. Blazor wil hier een bovengrens.</summary>
|
||||
private const int MaxTegelijk = 10;
|
||||
|
||||
/// <summary>Per persoon het nummer van zijn profielfoto, voor wie er een heeft.</summary>
|
||||
private IReadOnlyDictionary<long, long> Profielfotos = new Dictionary<long, long>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
var user = (await AuthState).User;
|
||||
_gebruikerId = user.UserId() ?? 0;
|
||||
_persoonId = user.PersonId() ?? 0;
|
||||
_mijnNaam = user.Naam() is { Length: > 0 } naam ? naam : "Mijn profiel";
|
||||
|
||||
var families = await FamilieOpslag.ListOwnedByPersonAsync(_persoonId);
|
||||
_familieId = families.FirstOrDefault()?.Id ?? 0;
|
||||
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Verversen()
|
||||
{
|
||||
if (_familieId == 0)
|
||||
{
|
||||
Leden = Array.Empty<FamilyMemberView>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var alle = await Families.ListMembersAsync(_familieId, _gebruikerId);
|
||||
Leden = alle.Where(l => l.PersonId != _persoonId).ToList();
|
||||
}
|
||||
|
||||
// De profielfoto's in één vraag erbij. Alleen het nummer; de browser haalt het plaatje
|
||||
// zelf op via /bestand/{id}/klein en bewaart het daarna in zijn eigen buffer.
|
||||
Profielfotos = (await Kluis.ProfielfotosAsync(Leden.Select(l => l.PersonId).Append(_persoonId),
|
||||
_gebruikerId))
|
||||
.ToDictionary(f => f.PersoonId, f => f.BestandId);
|
||||
}
|
||||
|
||||
/// <summary>Waar de profielfoto van deze persoon staat; null als hij er geen heeft.</summary>
|
||||
private string? Foto(long persoonId) =>
|
||||
Profielfotos.TryGetValue(persoonId, out var bestandId) ? $"/bestand/{bestandId}/klein" : null;
|
||||
|
||||
private void Nieuw()
|
||||
{
|
||||
Melding = null;
|
||||
Weghalen = null;
|
||||
BewerkLid = null;
|
||||
Bijlagen = Array.Empty<VaultFile>();
|
||||
Bewerken = new Formulier();
|
||||
}
|
||||
|
||||
private async Task Wijzigen(FamilyMemberView lid)
|
||||
{
|
||||
Melding = null;
|
||||
Weghalen = null;
|
||||
BewerkLid = lid;
|
||||
Bewerken = new Formulier
|
||||
{
|
||||
PersoonId = lid.PersonId,
|
||||
Voornaam = lid.FirstName,
|
||||
Achternaam = lid.LastName,
|
||||
Email = lid.Email ?? "",
|
||||
Relatie = lid.Relation,
|
||||
Geboortedatum = lid.BirthDate?.ToDateTime(TimeOnly.MinValue)
|
||||
};
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ foto's en documenten
|
||||
|
||||
private async Task BijlagenLaden()
|
||||
{
|
||||
if (Bewerken is not { PersoonId: > 0 } formulier) { Bijlagen = Array.Empty<VaultFile>(); return; }
|
||||
|
||||
var resultaat = await Kluis.ListPersonFilesAsync(formulier.PersoonId, _gebruikerId);
|
||||
Bijlagen = resultaat.Succeeded ? resultaat.Value! : Array.Empty<VaultFile>();
|
||||
}
|
||||
|
||||
private async Task BijlageKiezen(InputFileChangeEventArgs e)
|
||||
{
|
||||
if (Bewerken is not { PersoonId: > 0 } formulier) return;
|
||||
|
||||
Uploaden = true;
|
||||
Melding = null;
|
||||
try
|
||||
{
|
||||
foreach (var bestand in e.GetMultipleFiles(MaxTegelijk))
|
||||
{
|
||||
if (bestand.Size > VaultService.MaxBestandsgrootte)
|
||||
{
|
||||
Melding = $"{bestand.Name} is te groot. Maximaal {VaultService.MaxBestandsgrootte / 1024 / 1024} MB.";
|
||||
MeldingSoort = "fout";
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stroom = bestand.OpenReadStream(VaultService.MaxBestandsgrootte);
|
||||
using var buffer = new MemoryStream();
|
||||
await stroom.CopyToAsync(buffer);
|
||||
|
||||
var resultaat = await Kluis.StoreFileAsync(
|
||||
_gebruikerId, bestand.Name,
|
||||
string.IsNullOrWhiteSpace(bestand.ContentType) ? "application/octet-stream" : bestand.ContentType,
|
||||
buffer.ToArray(), subjectPersonId: formulier.PersoonId);
|
||||
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melding = $"Het bestand kon niet worden gelezen: {ex.Message}";
|
||||
MeldingSoort = "fout";
|
||||
}
|
||||
finally { Uploaden = false; }
|
||||
}
|
||||
|
||||
private async Task BijlageWeg(long bestandId)
|
||||
{
|
||||
var resultaat = await Kluis.DeleteFileAsync(bestandId, _gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
private async Task Profielfoto(long bestandId)
|
||||
{
|
||||
var resultaat = await Kluis.SetProfilePhotoAsync(bestandId, _gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
private async Task Opslaan()
|
||||
{
|
||||
var formulier = Bewerken!;
|
||||
if (string.IsNullOrWhiteSpace(formulier.Voornaam) && string.IsNullOrWhiteSpace(formulier.Achternaam))
|
||||
{
|
||||
Melding = "Vul minstens een voor- of achternaam in.";
|
||||
MeldingSoort = "fout";
|
||||
await JS.InvokeVoidAsync("mijnbesluit.springNaarVeld", "vn");
|
||||
return;
|
||||
}
|
||||
|
||||
var email = string.IsNullOrWhiteSpace(formulier.Email) ? null : formulier.Email.Trim();
|
||||
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
// AddMemberAsync geeft de nieuwe persoon terug, WijzigLidAsync alleen goed of fout.
|
||||
// Daarom hier twee aanroepen in plaats van een ternaire: de uitkomsttypen verschillen.
|
||||
bool gelukt;
|
||||
string? fout;
|
||||
|
||||
var geboren = formulier.Geboortedatum is { } g ? DateOnly.FromDateTime(g) : (DateOnly?)null;
|
||||
|
||||
if (formulier.PersoonId == 0)
|
||||
{
|
||||
var uitkomst = await Families.AddMemberAsync(
|
||||
_familieId, _gebruikerId, formulier.Voornaam.Trim(), formulier.Achternaam.Trim(),
|
||||
email, formulier.Relatie, null, geboren);
|
||||
gelukt = uitkomst.Succeeded;
|
||||
fout = uitkomst.Error;
|
||||
}
|
||||
else
|
||||
{
|
||||
var uitkomst = await Families.WijzigLidAsync(
|
||||
_familieId, formulier.PersoonId, _gebruikerId, formulier.Voornaam.Trim(),
|
||||
formulier.Achternaam.Trim(), email, formulier.Relatie, null, geboren);
|
||||
gelukt = uitkomst.Succeeded;
|
||||
fout = uitkomst.Error;
|
||||
}
|
||||
|
||||
if (gelukt)
|
||||
{
|
||||
Melding = formulier.PersoonId == 0 ? "Toegevoegd." : "De wijziging is opgeslagen.";
|
||||
MeldingSoort = "goed";
|
||||
Sluiten();
|
||||
await Verversen();
|
||||
}
|
||||
else
|
||||
{
|
||||
Melding = fout;
|
||||
MeldingSoort = "fout";
|
||||
}
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task Verwijderen(long persoonId)
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var resultaat = await Families.VerwijderLidAsync(_familieId, persoonId, _gebruikerId);
|
||||
Melding = resultaat.Succeeded ? "De persoon staat hier niet meer bij." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
if (resultaat.Succeeded) { Weghalen = null; await Verversen(); }
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task Uitnodigen(long persoonId)
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var resultaat = await Families.InviteAsync(_familieId, persoonId, _gebruikerId);
|
||||
Melding = resultaat.Succeeded ? "De uitnodiging is verstuurd." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
await Verversen();
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private void Sluiten()
|
||||
{
|
||||
Bewerken = null;
|
||||
BewerkLid = null;
|
||||
Bijlagen = Array.Empty<VaultFile>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// De regel onder de naam: "Partner (59)". Kort gehouden, want de status staat ernaast en
|
||||
/// die heeft ook ruimte nodig. Is iemand overleden, dan is dat belangrijker dan zijn leeftijd.
|
||||
/// </summary>
|
||||
private static string Onderregel(FamilyMemberView lid)
|
||||
{
|
||||
var relatie = Relatie(lid);
|
||||
|
||||
if (lid.DateOfDeath is { } overleden)
|
||||
return $"{relatie} · overleden {Datums.Datum(overleden)}";
|
||||
|
||||
return lid.BirthDate is { } geboren ? $"{relatie} ({Leeftijd(geboren)})" : relatie;
|
||||
}
|
||||
|
||||
/// <summary>Leeftijd in hele jaren, zoals je hem zelf zou uitrekenen.</summary>
|
||||
private static int Leeftijd(DateOnly geboren)
|
||||
{
|
||||
var vandaag = DateOnly.FromDateTime(DateTime.Today);
|
||||
var jaren = vandaag.Year - geboren.Year;
|
||||
if (geboren.AddYears(jaren) > vandaag) jaren--;
|
||||
return jaren;
|
||||
}
|
||||
|
||||
/// <summary>Een moment in UTC leesbaar op het scherm zetten, in lokale tijd.</summary>
|
||||
private static string Datum(DateTime moment) => Datums.Datum(moment);
|
||||
private static string KortDatum(DateTime moment) => Datums.Kort(moment);
|
||||
|
||||
private static string Relatie(FamilyMemberView lid) =>
|
||||
!string.IsNullOrWhiteSpace(lid.RelationLabel) ? lid.RelationLabel! : Omschrijving(lid.Relation);
|
||||
|
||||
// De lijst en de namen staan in het domein, zodat de website en de app er niet ieder een
|
||||
// eigen versie van hebben. Zie MijnBesluit.Domain.Rules.Relaties.
|
||||
private static string Omschrijving(RelationType r) => MijnBesluit.Domain.Rules.Relaties.Naam(r);
|
||||
|
||||
/// <summary>Eén formulier voor toevoegen en wijzigen; PersoonId 0 betekent nieuw.</summary>
|
||||
private class Formulier
|
||||
{
|
||||
public long PersoonId { get; set; }
|
||||
public string Voornaam { get; set; } = "";
|
||||
public string Achternaam { get; set; } = "";
|
||||
public string Email { get; set; } = "";
|
||||
// Partner staat bovenaan de lijst; een keuzelijst hoort te openen op wat je ziet staan.
|
||||
// Kind stond hier eerst, en dat is sinds de uitsplitsing geen keuze meer.
|
||||
public RelationType Relatie { get; set; } = RelationType.Partner;
|
||||
|
||||
/// <summary>Null als er geen geboortedatum is ingevuld; dat mag.</summary>
|
||||
public DateTime? Geboortedatum { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
@page "/"
|
||||
@attribute [Authorize]
|
||||
@inject OverzichtService Overzichten
|
||||
@inject LegacyService NalatenschapService
|
||||
@inject AdvertentieService Adverteren
|
||||
@inject AccountService Accounts
|
||||
@inject SamenvoegService Samenvoegen
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>MijnBesluit</PageTitle>
|
||||
|
||||
@* Het beginscherm, met dezelfde indeling als in de app: eerst wat er speelt, dan de tegels.
|
||||
De aantallen staan achter de titel in plaats van op een regel eronder, zodat de tegels
|
||||
laag blijven. *@
|
||||
|
||||
<AuthorizeView Context="auth">
|
||||
<Authorized>
|
||||
<h1>@Begroeting(auth.User.Naam())</h1>
|
||||
|
||||
@if (Fout is not null)
|
||||
{
|
||||
<div class="melding fout">@Fout</div>
|
||||
}
|
||||
|
||||
@if (Melding is not null)
|
||||
{
|
||||
<div class="melding goed">@Melding</div>
|
||||
}
|
||||
|
||||
@* Heeft iemand een profiel voor je aangemaakt op jouw adres? Dan staat die vraag hier,
|
||||
bovenaan, tot je hem beantwoordt. Alleen jij kunt zeggen of dat over jou gaat;
|
||||
koppelen op e-mailadres doet MijnBesluit niet. Zie docs/Identiteit.md. *@
|
||||
@foreach (var aantekening in Aantekeningen)
|
||||
{
|
||||
<div class="kaart" style="margin-bottom:1rem">
|
||||
<h2 style="margin-top:0">Ben jij dit?</h2>
|
||||
<p style="margin-top:0">
|
||||
<strong>@aantekening.Van</strong> heeft een profiel voor je aangemaakt op jouw
|
||||
e-mailadres@(aantekening.Relatie is { Length: > 0 } r ? $", als {r.ToLowerInvariant()}" : "").
|
||||
</p>
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Ben jij dat, dan voegen we het bij je eigen profiel: wat hij over je heeft
|
||||
ingevuld komt bij jou te staan, en vanaf dat moment bepaal jij wat er staat.
|
||||
Ben jij het niet, dan blijft alles zoals het is en vragen we het niet nog eens.
|
||||
</p>
|
||||
<div class="knoppen">
|
||||
<button class="knop vol" disabled="@Bezig"
|
||||
@onclick="() => Overnemen(aantekening.PersoonId)">Ja, dat ben ik</button>
|
||||
<button class="knop rustig" disabled="@Bezig"
|
||||
@onclick="() => Afwijzen(aantekening.PersoonId)">Nee, dat ben ik niet</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Gegevens is { Regels.Count: > 0 })
|
||||
{
|
||||
<div class="stapel speelt">
|
||||
@foreach (var regel in Gegevens.Regels)
|
||||
{
|
||||
<div class="regel @(regel.Soort == "overlijden" ? "let-op" : "")">
|
||||
<span class="teken">@Teken(regel.Soort)</span>
|
||||
<div class="groei">
|
||||
<div>@regel.Tekst</div>
|
||||
@if (regel.Datum is { } datum)
|
||||
{
|
||||
<div class="zacht">@Datums.MetDag(datum)</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Gegevens.Regels.Any(r => r.Soort == "overlijden"))
|
||||
{
|
||||
<button class="knop" style="margin-bottom:1rem" disabled="@Bezig" @onclick="IkBenErNog">
|
||||
Ik ben er nog
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@* De advertentie staat hier en niet bovenaan: "wat er speelt" gaat voor. Iemand die net
|
||||
leest dat er een overlijden op zijn naam is gemeld, hoort daar geen reclame boven te
|
||||
zien. Zie docs/Adverteren.md. *@
|
||||
@if (Reclame is not null)
|
||||
{
|
||||
<a class="advertentie" href="/advertentie/@Reclame.Id/uit"
|
||||
target="_blank" rel="noopener noreferrer nofollow">
|
||||
@if (Reclame.HeeftAfbeelding)
|
||||
{
|
||||
<img src="/advertentie/@Reclame.Id/afbeelding" alt="" />
|
||||
}
|
||||
<span class="groei">
|
||||
<span class="merkje">Advertentie</span>
|
||||
<span class="tekst">@Reclame.Tekst</span>
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
<div class="raster twee">
|
||||
<a class="kaart klikbaar" href="/kluis">
|
||||
<span class="groot">Mijn kluis <span class="aantal">@(Gegevens?.AantalKluis ?? 0)</span></span>
|
||||
</a>
|
||||
|
||||
<a class="kaart klikbaar" href="/familie">
|
||||
<span class="groot">Mijn mensen <span class="aantal">@(Gegevens?.AantalFamilie ?? 0)</span></span>
|
||||
</a>
|
||||
|
||||
@* Twee richtingen, dus twee aantallen; die staan achter de titel in hetzelfde
|
||||
rondje als de andere tegels, met hun naam erbij zodat je niet hoeft te raden
|
||||
welke welke is. Zo past de tegel op één regel, even hoog als de andere drie. *@
|
||||
<a class="kaart klikbaar" href="/momenten">
|
||||
<span class="groot">
|
||||
Momenten
|
||||
<span class="aantal" title="@(Gegevens?.AantalVoorMij ?? 0) voor mij, @(Gegevens?.AantalVanMij ?? 0) van mij">
|
||||
↓@(Gegevens?.AantalVoorMij ?? 0) · ↑@(Gegevens?.AantalVanMij ?? 0)
|
||||
</span>
|
||||
@if (Gegevens is { AantalNieuweBerichten: > 0 })
|
||||
{
|
||||
<span class="aantal nieuw">@Gegevens.AantalNieuweBerichten</span>
|
||||
}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a class="kaart klikbaar" href="/nalatenschap">
|
||||
<span class="groot">Nalatenschap <span class="aantal">@(Gegevens?.AantalBeheerders ?? 0)</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (Gegevens is not null)
|
||||
{
|
||||
<details class="uitklap" style="margin-top:1.5rem">
|
||||
<summary>Hoe ver vooruit wil je kijken?</summary>
|
||||
<p>
|
||||
Hierboven zie je wat er de komende tijd voor je klaarstaat. Zet het op 0 als je
|
||||
liever verrast wordt.
|
||||
</p>
|
||||
<div class="rij">
|
||||
<select @bind="Dagen" @bind:after="VooruitblikOpslaan" style="max-width:14rem">
|
||||
<option value="0">Niets vooruit tonen</option>
|
||||
<option value="7">7 dagen</option>
|
||||
<option value="14">14 dagen</option>
|
||||
<option value="30">30 dagen</option>
|
||||
<option value="60">60 dagen</option>
|
||||
</select>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
|
||||
@* Dezelfde uitleg als in de app en op de pagina /watismijnbesluit, maar hier als popup:
|
||||
je hoeft dan niet weg van je overzicht en weer terug.
|
||||
|
||||
De privacyverklaring staat er om dezelfde reden bij, en op dezelfde plek als in de app.
|
||||
Er is ook een pagina /privacy - die is er voor wie hem los wil kunnen delen en voor
|
||||
Google Play, dat een adres wil dat je kunt aanklikken. Maar wie hier zit, hoort hem te
|
||||
kunnen lezen zonder zijn overzicht kwijt te raken. De tekst zelf staat in
|
||||
Privacytekst.razor, hetzelfde onderdeel dat /privacy en de app gebruiken. *@
|
||||
<p style="margin-top:1rem">
|
||||
<button type="button" class="alsLink" @onclick="() => Uitleg = true">Wat is MijnBesluit?</button>
|
||||
<br />
|
||||
<button type="button" class="alsLink" @onclick="() => Privacy = true">Privacyverklaring</button>
|
||||
<br />
|
||||
<button type="button" class="alsLink" @onclick="() => VersiehistorieOpen = true">Versiehistorie</button>
|
||||
</p>
|
||||
|
||||
@if (Uitleg)
|
||||
{
|
||||
<Popup Titel="Wat is MijnBesluit?" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="() => Uitleg = false" OpSluiten="() => Uitleg = false">
|
||||
<Productuitleg />
|
||||
</Popup>
|
||||
}
|
||||
|
||||
@if (Privacy)
|
||||
{
|
||||
<Popup Titel="Privacy" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="() => Privacy = false" OpSluiten="() => Privacy = false">
|
||||
<Privacytekst />
|
||||
</Popup>
|
||||
}
|
||||
|
||||
@* Zelfde tekst als in de app: Versiehistorie.razor staat twee keer op schijf en een
|
||||
zelfcontrole vergelijkt ze letterlijk. *@
|
||||
@if (VersiehistorieOpen)
|
||||
{
|
||||
<Popup Titel="Versiehistorie" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="() => VersiehistorieOpen = false" OpSluiten="() => VersiehistorieOpen = false">
|
||||
<Versiehistorie />
|
||||
</Popup>
|
||||
}
|
||||
|
||||
<div class="kaart">
|
||||
<h2>Waar is MijnBesluit voor?</h2>
|
||||
<p class="zacht" style="margin-bottom:.8rem">
|
||||
Voor alles wat je zou willen doorgeven maar wat nu alleen in jouw hoofd zit.
|
||||
</p>
|
||||
<ul class="zacht" style="padding-left:1.2rem;margin:0">
|
||||
<li>
|
||||
<strong>Je kluis</strong> — wachtwoorden, de pincode van je telefoon,
|
||||
polissen, documenten, je uitvaartwensen. Alles versleuteld, en jij bepaalt per
|
||||
onderdeel of het alleen voor jou is, pas na je overlijden vrijkomt, of op een
|
||||
datum die je zelf kiest.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Je mensen</strong> — familie en vrienden die je toevoegt en
|
||||
uitnodigt. Zolang iemand de uitnodiging niet heeft aangenomen, beheer jij dat
|
||||
profiel; daarna is het van hem of haar.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Momenten</strong> — een tekst- of videobericht dat je klaarzet
|
||||
voor iemand. Te bezorgen op een datum die jij kiest, of pas na je overlijden.
|
||||
Een verjaardag die je niet meer meemaakt, of gewoon iets wat je nog wilt zeggen.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Je nalatenschap</strong> — wie er straks mag regelen wat er
|
||||
geregeld moet worden. Die persoon krijgt pas toegang als je overlijden is
|
||||
bevestigd door meerdere mensen én jij er zelf niet op hebt gereageerd.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private long _gebruikerId, _persoonId;
|
||||
private Overzicht? Gegevens;
|
||||
private int Dagen = 30;
|
||||
|
||||
/// <summary>Profielen die anderen op mijn adres hebben aangemaakt; alleen ik kan zeggen of ik dat ben.</summary>
|
||||
private IReadOnlyList<AccountService.Aantekeningaanbod> Aantekeningen =
|
||||
Array.Empty<AccountService.Aantekeningaanbod>();
|
||||
|
||||
/// <summary>Staat de uitleg open?</summary>
|
||||
private bool Uitleg;
|
||||
|
||||
/// <summary>Staat de privacyverklaring open?</summary>
|
||||
private bool Privacy;
|
||||
private bool VersiehistorieOpen;
|
||||
|
||||
/// <summary>De advertentie voor deze keer, of null als er geen is.</summary>
|
||||
private Campagne? Reclame;
|
||||
private bool Bezig;
|
||||
private string? Fout, Melding;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
var user = (await AuthState).User;
|
||||
_gebruikerId = user.UserId() ?? 0;
|
||||
_persoonId = user.PersonId() ?? 0;
|
||||
|
||||
await Laden();
|
||||
}
|
||||
|
||||
private async Task Laden()
|
||||
{
|
||||
if (_gebruikerId == 0) return;
|
||||
|
||||
var uitkomst = await Overzichten.VoorAsync(_gebruikerId);
|
||||
if (!uitkomst.Succeeded) { Fout = uitkomst.Error; return; }
|
||||
|
||||
Fout = null;
|
||||
Gegevens = uitkomst.Value;
|
||||
Dagen = Gegevens!.VooruitblikDagen;
|
||||
|
||||
// De keuze en het tellen horen bij elkaar: wat hier wordt uitgekozen komt ook echt in
|
||||
// beeld. Eén advertentie per keer dat het overzicht wordt geopend; zie docs/Adverteren.md.
|
||||
//
|
||||
// Gaat er hier iets mis, dan hoort het overzicht er gewoon te staan. Reclame is het
|
||||
// minst belangrijke op dit scherm.
|
||||
try
|
||||
{
|
||||
Reclame = await Adverteren.KiesAsync();
|
||||
if (Reclame is not null) await Adverteren.TelVertoningAsync(Reclame.Id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Reclame = null;
|
||||
}
|
||||
|
||||
// Heeft iemand een profiel voor mij aangemaakt? Apart opgehaald, en een fout hier hoort
|
||||
// het overzicht niet weg te houden: dit is een vraag die best een keer later mag komen.
|
||||
try { Aantekeningen = await Accounts.AantekeningenOverMijAsync(_gebruikerId); }
|
||||
catch (Exception) { Aantekeningen = Array.Empty<AccountService.Aantekeningaanbod>(); }
|
||||
}
|
||||
|
||||
/// <summary>"Ja, dat ben ik." Daarna staat alles wat die ander over je had bij jou.</summary>
|
||||
private async Task Overnemen(long persoonId)
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var uitkomst = await Accounts.NeemAantekeningOverAsync(persoonId, _gebruikerId, Samenvoegen);
|
||||
Fout = uitkomst.Succeeded ? null : uitkomst.Error;
|
||||
Melding = uitkomst.Succeeded ? "Klaar. Het profiel staat nu bij jou." : null;
|
||||
if (uitkomst.Succeeded) await Laden();
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
/// <summary>"Nee, dat ben ik niet." De vraag komt niet terug.</summary>
|
||||
private async Task Afwijzen(long persoonId)
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var uitkomst = await Accounts.WijsAantekeningAfAsync(persoonId, _gebruikerId);
|
||||
Fout = uitkomst.Succeeded ? null : uitkomst.Error;
|
||||
if (uitkomst.Succeeded)
|
||||
Aantekeningen = Aantekeningen.Where(a => a.PersoonId != persoonId).ToList();
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task VooruitblikOpslaan()
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var uitkomst = await Overzichten.VooruitblikAsync(_gebruikerId, Dagen);
|
||||
if (!uitkomst.Succeeded) { Fout = uitkomst.Error; return; }
|
||||
await Laden();
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task IkBenErNog()
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var uitkomst = await NalatenschapService.ObjectAsync(_persoonId, _gebruikerId);
|
||||
Melding = uitkomst.Succeeded ? "Genoteerd. De melding is ingetrokken." : null;
|
||||
Fout = uitkomst.Succeeded ? null : uitkomst.Error;
|
||||
if (uitkomst.Succeeded) await Laden();
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private static string Teken(string soort) => soort switch
|
||||
{
|
||||
"beheerder" => "\U0001F33F", // blad: iemand beheert jouw nalatenschap
|
||||
"beheer-voor" => "\U0001F91D", // handdruk: jij beheert die van een ander
|
||||
"moment" => "✉", // envelop
|
||||
"overlijden" => "⚠", // waarschuwing
|
||||
_ => "•"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// "Dag Jonathan" als we de naam kennen, anders alleen "Welkom". Nooit het e-mailadres:
|
||||
/// dat is geen naam, en het staat raar boven een scherm over je nalatenschap.
|
||||
/// </summary>
|
||||
private static string Begroeting(string naam)
|
||||
{
|
||||
var voornaam = naam.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||||
return string.IsNullOrWhiteSpace(voornaam) ? "Welkom" : $"Dag {voornaam}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
@page "/inloggen"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Authentication.Cookies
|
||||
@inject AccountService Accounts
|
||||
@inject IHttpContextAccessor HttpContexten
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
@* Deze pagina wordt statisch weergegeven: alleen zo kan er bij het inloggen een cookie worden
|
||||
gezet. Het gaat in twee stappen, net als in de WinDev-versie: eerst het e-mailadres, dan de
|
||||
code die per e-mail binnenkomt.
|
||||
|
||||
Let op bij het aanpassen: alle drie de formulieren worden ALTIJD gerenderd, en alleen
|
||||
verborgen met het hidden-attribuut. Bij een statische pagina is elke POST een nieuwe
|
||||
pagina-instantie; een formulier dat op dat moment niet gerenderd wordt, bestaat voor de
|
||||
server niet en levert "Cannot submit the form ... because no form on the page currently has
|
||||
that name" op. *@
|
||||
|
||||
<PageTitle>Inloggen - MijnBesluit</PageTitle>
|
||||
|
||||
@* "inlog-scherm": een merkteken voor app.css, zodat de kopbalk op een telefoon zijn logo kan
|
||||
laten vallen. Bij het inloggen mist er nog een cookie met de naam van de gebruiker; het
|
||||
logo is dan het enige dat de balk vult, en op een telefoon duwt dat het inlogblok een heel
|
||||
stuk lager dan nodig. Op een groter scherm is er ruimte genoeg en blijft het logo gewoon
|
||||
staan - zie de media-query bij ".inlog-scherm" in app.css. *@
|
||||
<div class="middenblok inlog-scherm">
|
||||
<h1>Log in</h1>
|
||||
|
||||
@if (Fout is not null) { <div class="melding fout">@Fout</div> }
|
||||
@if (Gelukt is not null) { <div class="melding goed">@Gelukt</div> }
|
||||
|
||||
<div hidden="@ToonCodeveld">
|
||||
<EditForm Model="Aanvraag" FormName="code-aanvragen" OnValidSubmit="CodeAanvragen" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<div class="veld">
|
||||
<label for="email">E-mailadres</label>
|
||||
<InputText id="email" @bind-Value="Aanvraag.Email" type="email"
|
||||
autocomplete="username" inputmode="email" placeholder="alles in kleine letters" />
|
||||
<ValidationMessage For="() => Aanvraag.Email" class="uitleg" />
|
||||
</div>
|
||||
|
||||
<details class="uitklap">
|
||||
<summary>Hoe werkt inloggen?</summary>
|
||||
<p>
|
||||
Wij sturen naar het opgegeven e-mailadres een code om het inloggen af te ronden.
|
||||
Die code vul je hierna in. Je hoeft dus geen wachtwoord te onthouden.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<button type="submit" class="knop vol">Code aanvragen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<div hidden="@(!ToonCodeveld)">
|
||||
<EditForm Model="Inlog" FormName="inloggen" OnValidSubmit="Aanmelden" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<div class="veld">
|
||||
<label for="email2">E-mailadres</label>
|
||||
<InputText id="email2" @bind-Value="Inlog.Email" type="email" readonly />
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="code">Code</label>
|
||||
<InputText id="code" @bind-Value="Inlog.Code" class="codeveld" inputmode="numeric"
|
||||
autocomplete="one-time-code" maxlength="6" />
|
||||
<div class="uitleg">
|
||||
Vul hier de code in die wij hebben gestuurd naar
|
||||
<span class="naar-adres">@AfgeschermdAdres</span>.
|
||||
De code is @((int)AccountService.CodeGeldigheid.TotalMinutes) minuten geldig.
|
||||
</div>
|
||||
<ValidationMessage For="() => Inlog.Code" class="uitleg" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="knop vol">Inloggen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
<EditForm Model="Opnieuw" FormName="nieuwe-code" OnValidSubmit="OpnieuwAanvragen" method="post">
|
||||
<div class="kaart">
|
||||
<InputText @bind-Value="Opnieuw.Email" type="hidden" />
|
||||
<div class="rij">
|
||||
<div class="groei zacht">Geen e-mail ontvangen? Kijk ook even in je ongewenste post.</div>
|
||||
<button type="submit" class="knop rustig">Nieuwe code aanvragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<p class="zacht"><a href="/wat-is-mijnbesluit">Wat is MijnBesluit?</a></p>
|
||||
<p class="zacht"><a href="/registreren">Ik ben nieuw en wil een account aanmaken</a></p>
|
||||
<p class="zacht"><a href="/adverteren">Ik wil adverteren op MijnBesluit</a></p>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm(FormName = "code-aanvragen")] private AanvraagGegevens Aanvraag { get; set; } = new();
|
||||
[SupplyParameterFromForm(FormName = "inloggen")] private InlogGegevens Inlog { get; set; } = new();
|
||||
[SupplyParameterFromForm(FormName = "nieuwe-code")] private AanvraagGegevens Opnieuw { get; set; } = new();
|
||||
[SupplyParameterFromQuery(Name = "terug")] private string? Terug { get; set; }
|
||||
|
||||
private bool _codeVerstuurd;
|
||||
private string? AfgeschermdAdres;
|
||||
private string? Fout, Gelukt;
|
||||
|
||||
/// <summary>
|
||||
/// Staat er al een e-mailadres in een van de formulieren van stap twee, dan is die stap
|
||||
/// eerder bereikt en horen we het codeveld te tonen — ook bij de eerste render van een POST,
|
||||
/// vóórdat de afhandeling heeft gedraaid.
|
||||
/// </summary>
|
||||
private bool ToonCodeveld =>
|
||||
_codeVerstuurd || !string.IsNullOrWhiteSpace(Inlog.Email) || !string.IsNullOrWhiteSpace(Opnieuw.Email);
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (AfgeschermdAdres is null && ToonCodeveld)
|
||||
{
|
||||
var adres = !string.IsNullOrWhiteSpace(Inlog.Email) ? Inlog.Email : Opnieuw.Email;
|
||||
AfgeschermdAdres = Afschermen(adres);
|
||||
}
|
||||
}
|
||||
|
||||
private Task CodeAanvragen() => StuurCodeAsync(Aanvraag.Email);
|
||||
|
||||
private Task OpnieuwAanvragen() => StuurCodeAsync(Opnieuw.Email);
|
||||
|
||||
private async Task StuurCodeAsync(string email)
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
var resultaat = await Accounts.VraagInlogcodeAanAsync(
|
||||
email, context.Connection.RemoteIpAddress?.ToString(), context.RequestAborted);
|
||||
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
Fout = resultaat.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_codeVerstuurd = true;
|
||||
AfgeschermdAdres = resultaat.Value!.AfgeschermdAdres;
|
||||
Inlog = new InlogGegevens { Email = resultaat.Value.Email };
|
||||
Opnieuw = new AanvraagGegevens { Email = resultaat.Value.Email };
|
||||
Gelukt = "We hebben je een code gestuurd. Vul hem hieronder in.";
|
||||
Fout = null;
|
||||
}
|
||||
|
||||
private async Task Aanmelden()
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
var resultaat = await Accounts.LogInMetCodeAsync(Inlog.Email, Inlog.Code, context.RequestAborted);
|
||||
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
// Het codeveld blijft staan, met de melding erboven.
|
||||
_codeVerstuurd = true;
|
||||
Opnieuw = new AanvraagGegevens { Email = Inlog.Email };
|
||||
AfgeschermdAdres = Afschermen(Inlog.Email);
|
||||
Fout = resultaat.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await MeldAanAsync(context, resultaat.Value!);
|
||||
Navigatie.NavigateTo(VeiligeBestemming(Terug), forceLoad: true);
|
||||
}
|
||||
|
||||
/// <summary>Zet het inlogcookie met wie er is ingelogd.</summary>
|
||||
internal static async Task MeldAanAsync(HttpContext context, User gebruiker)
|
||||
{
|
||||
// DisplayName is een lege tekst als er geen naam is ingevuld, niet null - de oude
|
||||
// terugval met ?? greep daar dus niet op en liet het e-mailadres staan. Een adres is
|
||||
// geen naam; staat er niets, dan laat het scherm de begroeting gewoon weg.
|
||||
var naam = gebruiker.Person?.DisplayName;
|
||||
if (string.IsNullOrWhiteSpace(naam)) naam = "";
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, gebruiker.Id.ToString()),
|
||||
new(ClaimTypes.Email, gebruiker.Email),
|
||||
new(ClaimTypes.Name, naam),
|
||||
new(HuidigeGebruiker.PersonIdClaim, gebruiker.PersonId.ToString()),
|
||||
new("stamp", gebruiker.SecurityStamp.ToString()),
|
||||
// Zonder deze claim toont MainLayout het menu-item "Beheer" nooit, ook niet voor
|
||||
// wie in de database wel IsBeheerder = true heeft staan - dat is precies wat hij
|
||||
// moet aansturen, zie HuidigeGebruiker.BeheerClaim.
|
||||
new(HuidigeGebruiker.BeheerClaim, gebruiker.IsBeheerder ? "ja" : "nee")
|
||||
};
|
||||
|
||||
await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)));
|
||||
}
|
||||
|
||||
/// <summary>Accepteert alleen paden binnen de site, zodat een link niet naar buiten kan doorsturen.</summary>
|
||||
private static string VeiligeBestemming(string? terug) =>
|
||||
string.IsNullOrWhiteSpace(terug) || terug.Contains("//") || terug.StartsWith('/') ? "/" : "/" + terug;
|
||||
|
||||
private static string Afschermen(string email) => new CodeVerstuurd(email, DateTime.UtcNow).AfgeschermdAdres;
|
||||
|
||||
public class AanvraagGegevens
|
||||
{
|
||||
[Required(ErrorMessage = "Je moet een e-mailadres invullen!")]
|
||||
[EmailAddress(ErrorMessage = "Dit is geen geldig e-mailadres!")]
|
||||
public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
public class InlogGegevens
|
||||
{
|
||||
[Required] public string Email { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Je moet een code invullen!")]
|
||||
public string Code { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
@page "/kluis"
|
||||
@attribute [Authorize]
|
||||
@inject VaultService KluisService
|
||||
@inject LegacyService BeheerService
|
||||
@inject IJSRuntime JS
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Mijn kluis - MijnBesluit</PageTitle>
|
||||
|
||||
<h1>Mijn kluis</h1>
|
||||
<p class="zacht">
|
||||
Wat je hier vastlegt staat versleuteld opgeslagen. Per item bepaal je zelf of het privé blijft,
|
||||
of dat het vrijkomt na je overlijden of op een datum die jij kiest.
|
||||
</p>
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
<div class="kaart">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<label for="cat">Soort</label>
|
||||
<select id="cat" @bind="Filter" @bind:after="Verversen">
|
||||
<option value="">Alles</option>
|
||||
@foreach (var c in Categorieen)
|
||||
{
|
||||
<option value="@((int)c)">@Naam(c)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
@* Geen "Sluiten" meer: het formulier staat nu in een popup en die heeft zijn eigen
|
||||
Annuleren. Deze knop begint altijd met een leeg item. *@
|
||||
<button class="knop" @onclick="Beginnen">Toevoegen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (ToonFormulier)
|
||||
{
|
||||
@* De EditForm staat om de popup heen en niet erin: de invoervelden hebben zijn context
|
||||
nodig, en de knoppen zitten in de popup zelf. Enter in een veld slaat nog steeds op. *@
|
||||
<EditForm Model="Nieuw" OnValidSubmit="Opslaan" FormName="kluisitem">
|
||||
<Popup Titel="@(BewerkId == 0 ? "Iets vastleggen" : "Item wijzigen")"
|
||||
Bevestigtekst="@(BewerkId == 0 ? "Opslaan in de kluis" : "Wijziging opslaan")"
|
||||
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>
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="titel">Titel</label>
|
||||
<InputText id="titel" @bind-Value="Nieuw.Titel" placeholder="@Voorbeeld(Nieuw.Categorie)" />
|
||||
<div class="uitleg">De titel is niet versleuteld, zodat je kunt zoeken. Zet er dus niets geheims in.</div>
|
||||
</div>
|
||||
|
||||
@switch (Nieuw.Categorie)
|
||||
{
|
||||
case VaultCategory.Wachtwoord:
|
||||
<div class="raster twee">
|
||||
<div class="veld"><label>Website</label><InputText @bind-Value="Nieuw.Website" /></div>
|
||||
<div class="veld"><label>Gebruikersnaam</label><InputText @bind-Value="Nieuw.Gebruikersnaam" /></div>
|
||||
</div>
|
||||
<div class="veld"><label>Wachtwoord</label><InputText @bind-Value="Nieuw.Geheim" type="password" /></div>
|
||||
break;
|
||||
|
||||
case VaultCategory.SocialMedia:
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label>Welk medium?</label>
|
||||
<InputSelect @bind-Value="Nieuw.Platform">
|
||||
<option value="">Kies…</option>
|
||||
@foreach (var naam in SocialeMedia.Namen)
|
||||
{
|
||||
<option value="@naam">@naam</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="veld"><label>Gebruikersnaam</label><InputText @bind-Value="Nieuw.Gebruikersnaam" /></div>
|
||||
</div>
|
||||
@if (Nieuw.Platform == SocialeMedia.Anders)
|
||||
{
|
||||
<div class="veld"><label>Welke dan?</label><InputText @bind-Value="Nieuw.PlatformAnders" /></div>
|
||||
}
|
||||
<div class="veld"><label>Wachtwoord</label><InputText @bind-Value="Nieuw.Geheim" type="password" /></div>
|
||||
<div class="veld">
|
||||
<label>Herstelcodes</label>
|
||||
<InputTextArea @bind-Value="Nieuw.Herstelcodes" />
|
||||
<div class="uitleg">De codes die je krijgt bij tweestapsverificatie. Zonder die codes komt niemand er nog in.</div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case VaultCategory.Apparaat:
|
||||
<div class="veld"><label>Apparaat</label><InputText @bind-Value="Nieuw.Apparaat" /></div>
|
||||
<div class="veld"><label>Gebruikersnaam</label><InputText @bind-Value="Nieuw.Gebruikersnaam" /></div>
|
||||
<div class="veld"><label>Wachtwoord of code</label><InputText @bind-Value="Nieuw.Geheim" type="password" /></div>
|
||||
break;
|
||||
|
||||
case VaultCategory.Pincode:
|
||||
<div class="veld"><label>Waarvan</label><InputText @bind-Value="Nieuw.Apparaat" placeholder="Mijn telefoon" /></div>
|
||||
<div class="veld"><label>Pincode</label><InputText @bind-Value="Nieuw.Pincode" type="password" /></div>
|
||||
break;
|
||||
|
||||
case VaultCategory.Polis:
|
||||
<div class="raster twee">
|
||||
<div class="veld"><label>Verzekeraar</label><InputText @bind-Value="Nieuw.Verzekeraar" /></div>
|
||||
<div class="veld"><label>Polisnummer</label><InputText @bind-Value="Nieuw.Polisnummer" /></div>
|
||||
</div>
|
||||
<div class="veld"><label>Telefoonnummer</label><InputText @bind-Value="Nieuw.Telefoon" /></div>
|
||||
break;
|
||||
|
||||
case VaultCategory.Uitvaartwens:
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label>Begraven of cremeren</label>
|
||||
<InputSelect @bind-Value="Nieuw.BegravenOfCremeren">
|
||||
<option value="">Nog niet gekozen</option>
|
||||
<option value="Begraven">Begraven</option>
|
||||
<option value="Cremeren">Cremeren</option>
|
||||
<option value="Aan de wetenschap">Aan de wetenschap</option>
|
||||
<option value="Nabestaanden beslissen">Nabestaanden beslissen</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="veld"><label>Locatie</label><InputText @bind-Value="Nieuw.Locatie" /></div>
|
||||
</div>
|
||||
<div class="veld"><label>Muziek</label><InputTextArea id="muziek" rows="1" @bind-Value="Nieuw.Muziek" /></div>
|
||||
<div class="veld"><label>Wie mag spreken</label><InputText @bind-Value="Nieuw.Sprekers" /></div>
|
||||
<div class="veld"><label>Verdere wensen</label><InputTextArea id="wensen" rows="4" @bind-Value="Nieuw.Wensen" /></div>
|
||||
break;
|
||||
|
||||
case VaultCategory.Donorcodicil:
|
||||
break;
|
||||
}
|
||||
|
||||
@if (Nieuw.Categorie is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil)
|
||||
{
|
||||
<div class="veld">
|
||||
<label>Bespreken met</label>
|
||||
<InputSelect @bind-Value="Nieuw.BesprekenMetPersonId">
|
||||
<option value="">Niemand (alleen voor mijzelf)</option>
|
||||
@if (Beheerders is not null)
|
||||
{
|
||||
@foreach (var b in Beheerders)
|
||||
{
|
||||
<option value="@b.PersonId">@b.DisplayName</option>
|
||||
}
|
||||
}
|
||||
</InputSelect>
|
||||
<p class="zacht klein">
|
||||
Kies je hier iemand, dan kan diegene dit item meteen lezen en met je bespreken -
|
||||
je hoeft niet te wachten tot na je overlijden. Kies je niemand, dan blijft dit item
|
||||
zoals altijd alleen voor jezelf.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="veld">
|
||||
<label for="notitie">Toelichting</label>
|
||||
<InputTextArea id="notitie" @bind-Value="Nieuw.Notitie" />
|
||||
</div>
|
||||
|
||||
@if (BewerkId == 0)
|
||||
{
|
||||
<div class="veld">
|
||||
<label>Foto's en documenten</label>
|
||||
<div class="uitleg">
|
||||
Die kun je toevoegen zodra dit is opgeslagen; ze horen bij het item en
|
||||
dat moet er eerst zijn.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Bijlagenblok Bijlagen="@(Bijlagen ?? Array.Empty<VaultFile>())" Bezig="@Uploaden"
|
||||
Uitleg="Een foto van je polis, een scan van je testament, de handleiding van de kluis zelf."
|
||||
OpKiezen="BijlageKiezen"
|
||||
OpVerwijderen="BijlageWeg" />
|
||||
}
|
||||
|
||||
<div class="veld">
|
||||
<label>Wanneer mag een ander dit zien?</label>
|
||||
<div class="keuze">
|
||||
<input type="radio" id="r0" name="vrijgave" checked="@(Nieuw.Vrijgave == ReleasePolicy.AlleenIkzelf)"
|
||||
@onchange="@(() => Nieuw.Vrijgave = ReleasePolicy.AlleenIkzelf)" />
|
||||
<label for="r0">Nooit — alleen ik kan hier bij</label>
|
||||
</div>
|
||||
<div class="keuze">
|
||||
<input type="radio" id="r1" name="vrijgave" checked="@(Nieuw.Vrijgave == ReleasePolicy.NaOverlijden)"
|
||||
@onchange="@(() => Nieuw.Vrijgave = ReleasePolicy.NaOverlijden)" />
|
||||
<label for="r1">Na mijn overlijden, voor mijn nalatenschapsbeheerder</label>
|
||||
</div>
|
||||
<div class="keuze">
|
||||
<input type="radio" id="r2" name="vrijgave" checked="@(Nieuw.Vrijgave == ReleasePolicy.OpDatum)"
|
||||
@onchange="@(() => Nieuw.Vrijgave = ReleasePolicy.OpDatum)" />
|
||||
<label for="r2">Op een datum</label>
|
||||
</div>
|
||||
@if (Nieuw.Vrijgave == ReleasePolicy.OpDatum)
|
||||
{
|
||||
<InputDate @bind-Value="Nieuw.VrijgaveDatum" />
|
||||
}
|
||||
</div>
|
||||
|
||||
</Popup>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@if (Items is null)
|
||||
{
|
||||
<p class="zacht">Bezig met laden…</p>
|
||||
}
|
||||
else if (Items.Count == 0)
|
||||
{
|
||||
<div class="leeg">
|
||||
<span class="teken">🔒</span>
|
||||
Je kluis is nog leeg. Begin bijvoorbeeld met je uitvaartwensen.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stapel">
|
||||
@foreach (var item in Items)
|
||||
{
|
||||
<div class="kaart">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<div class="groot">@item.Title</div>
|
||||
<div class="zacht">@Naam(item.Category) · @Vrijgave(item)</div>
|
||||
@if (item.SharedManagerPersonId is { } gedeeldMet)
|
||||
{
|
||||
<div class="zacht">💬 Wordt besproken met @BeheerderNaam(gedeeldMet)</div>
|
||||
}
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => Openen(item.Id)">
|
||||
@(Geopend == item.Id ? "Verbergen" : "Bekijken")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (Weghalen == item.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.9rem">
|
||||
<strong>@item.Title</strong> verwijderen? Wat erin staat is daarna weg.
|
||||
<div class="rij" style="margin-top:.7rem">
|
||||
<button class="knop gevaar" @onclick="() => Verwijderen(item.Id)">Ja, verwijderen</button>
|
||||
<button class="knop rustig" @onclick="() => Weghalen = null">Nee, bewaren</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="rij" style="margin-top:.9rem">
|
||||
<button class="knop rustig" @onclick="() => Wijzigen(item)">Wijzigen</button>
|
||||
<button class="knop rustig" @onclick="() => Weghalen = item.Id">Verwijderen</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Geopend == item.Id && Inhoud is not null)
|
||||
{
|
||||
<hr />
|
||||
<dl style="margin:0">
|
||||
@foreach (var (label, waarde) in Velden(Inhoud))
|
||||
{
|
||||
<div class="rij" style="padding:.25rem 0">
|
||||
<span class="zacht" style="min-width:150px">@label</span>
|
||||
<span class="groei">@waarde</span>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
|
||||
<hr />
|
||||
<h3>Bijlagen</h3>
|
||||
|
||||
@if (Bijlagen is { Count: > 0 })
|
||||
{
|
||||
<div class="stapel">
|
||||
@foreach (var bijlage in Bijlagen)
|
||||
{
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<a href="/kluis/bestand/@bijlage.Id" target="_blank">@bijlage.FileName</a>
|
||||
<div class="zacht">@Grootte(bijlage.SizeBytes) · @Datums.Datum(bijlage.CreatedAt)</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => WeghalenBijlage = bijlage.Id">Verwijderen</button>
|
||||
</div>
|
||||
|
||||
@if (WeghalenBijlage == bijlage.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-bottom:.6rem">
|
||||
<strong>@bijlage.FileName</strong> verwijderen?
|
||||
<div class="rij" style="margin-top:.5rem">
|
||||
<button class="knop gevaar" @onclick="() => BijlageWegBevestigd(bijlage.Id)">Ja, verwijderen</button>
|
||||
<button class="knop rustig" @onclick="() => WeghalenBijlage = null">Nee, laat staan</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (bijlage.ContentType.StartsWith("image/"))
|
||||
{
|
||||
<img src="/kluis/bestand/@bijlage.Id" alt="@bijlage.FileName"
|
||||
style="max-width:100%;border-radius:10px;margin:.4rem 0 1rem" />
|
||||
}
|
||||
else if (bijlage.ContentType.StartsWith("video/"))
|
||||
{
|
||||
<video src="/kluis/bestand/@bijlage.Id" controls
|
||||
style="max-width:100%;border-radius:10px;margin:.4rem 0 1rem"></video>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="zacht">Nog geen bijlagen. Denk aan een foto van je polis, of een scan van je testament.</p>
|
||||
}
|
||||
|
||||
<div class="veld" style="margin-top:.8rem">
|
||||
<label for="bijlage">Bestand toevoegen</label>
|
||||
<InputFile id="bijlage" OnChange="BijlageKiezen" disabled="@Uploaden" multiple />
|
||||
<div class="uitleg">
|
||||
Maximaal @(VaultService.MaxBestandsgrootte / 1024 / 1024) MB per bestand. Alles wordt
|
||||
versleuteld opgeslagen, net als de rest van je kluis.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Uploaden)
|
||||
{
|
||||
<p class="zacht">Bezig met opslaan…</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private long _gebruikerId, _persoonId;
|
||||
private IReadOnlyList<VaultItem>? Items;
|
||||
private VaultPayload? Inhoud;
|
||||
private long? Geopend;
|
||||
private bool ToonFormulier;
|
||||
private string Filter = "";
|
||||
private string? Melding, MeldingSoort;
|
||||
private NieuwItem Nieuw = new();
|
||||
private IReadOnlyList<LegacyService.BeheerderView>? Beheerders;
|
||||
|
||||
/// <summary>Het item dat bewerkt wordt; 0 betekent een nieuw item.</summary>
|
||||
private long BewerkId;
|
||||
|
||||
/// <summary>Het item dat op verwijderen staat te wachten.</summary>
|
||||
private long? Weghalen;
|
||||
|
||||
/// <summary>Welke bijlage bij het bekeken item op een "weet je het zeker?" staat te wachten.</summary>
|
||||
private long? WeghalenBijlage;
|
||||
|
||||
/// <summary>De bijlagen bij het item dat openstaat of dat in het wijzigscherm staat.</summary>
|
||||
private IReadOnlyList<VaultFile>? Bijlagen;
|
||||
private bool Uploaden;
|
||||
|
||||
/// <summary>Zoveel bestanden tegelijk kiezen mag. Blazor wil hier een bovengrens.</summary>
|
||||
private const int MaxTegelijk = 10;
|
||||
|
||||
private static readonly VaultCategory[] Categorieen =
|
||||
{
|
||||
VaultCategory.Uitvaartwens, VaultCategory.Donorcodicil, VaultCategory.Polis, VaultCategory.Wachtwoord,
|
||||
VaultCategory.SocialMedia, VaultCategory.Apparaat, VaultCategory.Pincode,
|
||||
VaultCategory.Rijbewijs, VaultCategory.Paspoort,
|
||||
VaultCategory.Document, VaultCategory.Notitie, VaultCategory.Overig
|
||||
};
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (ToonFormulier)
|
||||
{
|
||||
await JS.InvokeVoidAsync("mijnbesluit.laatMeegroeien", "muziek");
|
||||
await JS.InvokeVoidAsync("mijnbesluit.laatMeegroeien", "wensen");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
var user = (await AuthState).User;
|
||||
_gebruikerId = user.UserId() ?? 0;
|
||||
_persoonId = user.PersonId() ?? 0;
|
||||
Beheerders = await BeheerService.ListManagersAsync(_persoonId);
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Verversen()
|
||||
{
|
||||
VaultCategory? categorie = int.TryParse(Filter, out var c) ? (VaultCategory)c : null;
|
||||
Items = await KluisService.ListAsync(_persoonId, categorie);
|
||||
Geopend = null;
|
||||
Inhoud = null;
|
||||
}
|
||||
|
||||
private async Task Openen(long id)
|
||||
{
|
||||
if (Geopend == id) { Geopend = null; Inhoud = null; Bijlagen = null; return; }
|
||||
|
||||
var resultaat = await KluisService.OpenAsync(id, _gebruikerId);
|
||||
if (resultaat.Succeeded)
|
||||
{
|
||||
Geopend = id;
|
||||
Inhoud = resultaat.Value;
|
||||
Melding = null;
|
||||
await BijlagenLaden();
|
||||
}
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bij welk item een bijlage hoort: het item dat in het wijzigscherm staat, of het item dat
|
||||
/// je hebt opengeklapt. Zo werken de knoppen op allebei de plekken.
|
||||
/// </summary>
|
||||
private long? BijlageDoel => BewerkId != 0 ? BewerkId : Geopend;
|
||||
|
||||
private async Task BijlagenLaden()
|
||||
{
|
||||
if (BijlageDoel is not { } id) { Bijlagen = null; return; }
|
||||
var resultaat = await KluisService.ListFilesAsync(id, _gebruikerId);
|
||||
Bijlagen = resultaat.Succeeded ? resultaat.Value : Array.Empty<VaultFile>();
|
||||
}
|
||||
|
||||
private async Task BijlageKiezen(InputFileChangeEventArgs e)
|
||||
{
|
||||
if (BijlageDoel is not { } itemId) return;
|
||||
|
||||
Uploaden = true;
|
||||
Melding = null;
|
||||
try
|
||||
{
|
||||
foreach (var bestand in e.GetMultipleFiles(MaxTegelijk))
|
||||
{
|
||||
if (bestand.Size > VaultService.MaxBestandsgrootte)
|
||||
{
|
||||
Melding = $"{bestand.Name} is te groot. Maximaal {VaultService.MaxBestandsgrootte / 1024 / 1024} MB.";
|
||||
MeldingSoort = "fout";
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stroom = bestand.OpenReadStream(VaultService.MaxBestandsgrootte);
|
||||
using var buffer = new MemoryStream();
|
||||
await stroom.CopyToAsync(buffer);
|
||||
|
||||
var resultaat = await KluisService.StoreFileAsync(_gebruikerId, bestand.Name,
|
||||
string.IsNullOrWhiteSpace(bestand.ContentType) ? "application/octet-stream" : bestand.ContentType,
|
||||
buffer.ToArray(), itemId);
|
||||
|
||||
if (resultaat.Succeeded) { Melding = $"{bestand.Name} is toegevoegd."; MeldingSoort = "goed"; }
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melding = $"Het bestand kon niet worden gelezen: {ex.Message}";
|
||||
MeldingSoort = "fout";
|
||||
}
|
||||
finally
|
||||
{
|
||||
Uploaden = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BijlageWeg(long bestandId)
|
||||
{
|
||||
var resultaat = await KluisService.DeleteFileAsync(bestandId, _gebruikerId);
|
||||
Melding = resultaat.Succeeded ? "De bijlage is verwijderd." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
private async Task BijlageWegBevestigd(long bestandId)
|
||||
{
|
||||
WeghalenBijlage = null;
|
||||
await BijlageWeg(bestandId);
|
||||
}
|
||||
|
||||
private static string Grootte(long bytes) => bytes >= 1024 * 1024
|
||||
? $"{bytes / 1024d / 1024d:0.#} MB"
|
||||
: $"{Math.Max(1, bytes / 1024)} kB";
|
||||
|
||||
private async Task Wijzigen(VaultItem item)
|
||||
{
|
||||
// De inhoud staat versleuteld; die halen we op om het formulier te vullen.
|
||||
var resultaat = await KluisService.OpenAsync(item.Id, _gebruikerId);
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
Melding = resultaat.Error;
|
||||
MeldingSoort = "fout";
|
||||
return;
|
||||
}
|
||||
|
||||
var p = resultaat.Value!;
|
||||
Nieuw = new NieuwItem
|
||||
{
|
||||
Categorie = item.Category,
|
||||
Titel = item.Title,
|
||||
Notitie = p.Notes ?? "",
|
||||
Website = p.Website ?? "",
|
||||
Gebruikersnaam = p.Username ?? "",
|
||||
Geheim = p.Secret ?? "",
|
||||
Herstelcodes = p.RecoveryCodes ?? "",
|
||||
Platform = p.Platform ?? "",
|
||||
PlatformAnders = p.PlatformAnders ?? "",
|
||||
Pincode = p.Pincode ?? "",
|
||||
Apparaat = p.DeviceName ?? "",
|
||||
Verzekeraar = p.Insurer ?? "",
|
||||
Polisnummer = p.PolicyNumber ?? "",
|
||||
Telefoon = p.ContactPhone ?? "",
|
||||
BegravenOfCremeren = p.BurialOrCremation ?? "",
|
||||
Locatie = p.Location ?? "",
|
||||
Muziek = p.Music ?? "",
|
||||
Sprekers = p.Speakers ?? "",
|
||||
Wensen = p.Wishes ?? "",
|
||||
BesprekenMetPersonId = item.SharedManagerPersonId,
|
||||
Vrijgave = item.ReleasePolicy,
|
||||
VrijgaveDatum = item.ReleaseAt is { } r ? DateOnly.FromDateTime(r) : null
|
||||
};
|
||||
|
||||
BewerkId = item.Id;
|
||||
ToonFormulier = true;
|
||||
Weghalen = null;
|
||||
Geopend = null;
|
||||
Inhoud = null;
|
||||
Melding = null;
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
/// <summary>Opent het formulier met een leeg item, ook als er net iets anders openstond.</summary>
|
||||
private void Beginnen()
|
||||
{
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwItem();
|
||||
Bijlagen = null;
|
||||
ToonFormulier = true;
|
||||
}
|
||||
|
||||
private void Annuleren()
|
||||
{
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwItem();
|
||||
Bijlagen = null;
|
||||
ToonFormulier = false;
|
||||
}
|
||||
|
||||
private async Task Verwijderen(long id)
|
||||
{
|
||||
var resultaat = await KluisService.DeleteAsync(id, _gebruikerId);
|
||||
Melding = resultaat.Succeeded ? "Het item is verwijderd." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
Weghalen = null;
|
||||
if (resultaat.Succeeded) await Verversen();
|
||||
}
|
||||
|
||||
private async Task Opslaan()
|
||||
{
|
||||
var payload = new VaultPayload
|
||||
{
|
||||
Notes = Leeg(Nieuw.Notitie),
|
||||
Website = Leeg(Nieuw.Website),
|
||||
Username = Leeg(Nieuw.Gebruikersnaam),
|
||||
Secret = Leeg(Nieuw.Geheim),
|
||||
RecoveryCodes = Leeg(Nieuw.Herstelcodes),
|
||||
Platform = Leeg(Nieuw.Platform),
|
||||
PlatformAnders = Leeg(Nieuw.PlatformAnders),
|
||||
Pincode = Leeg(Nieuw.Pincode),
|
||||
DeviceName = Leeg(Nieuw.Apparaat),
|
||||
Insurer = Leeg(Nieuw.Verzekeraar),
|
||||
PolicyNumber = Leeg(Nieuw.Polisnummer),
|
||||
ContactPhone = Leeg(Nieuw.Telefoon),
|
||||
BurialOrCremation = Leeg(Nieuw.BegravenOfCremeren),
|
||||
Location = Leeg(Nieuw.Locatie),
|
||||
Music = Leeg(Nieuw.Muziek),
|
||||
Speakers = Leeg(Nieuw.Sprekers),
|
||||
Wishes = Leeg(Nieuw.Wensen)
|
||||
};
|
||||
|
||||
DateTime? datum = Nieuw.Vrijgave == ReleasePolicy.OpDatum && Nieuw.VrijgaveDatum is { } d
|
||||
? d.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc) : null;
|
||||
|
||||
// Nieuw geeft het item terug, wijzigen alleen goed of fout; hier maken we er één uitkomst van.
|
||||
Result resultaat;
|
||||
if (BewerkId == 0)
|
||||
{
|
||||
var gemaakt = await KluisService.CreateAsync(_gebruikerId, Nieuw.Categorie, Nieuw.Titel, payload,
|
||||
Nieuw.Vrijgave, datum, Nieuw.BesprekenMetPersonId);
|
||||
resultaat = gemaakt.Succeeded ? Result.Ok() : Result.Fail(gemaakt.Error!);
|
||||
}
|
||||
else
|
||||
{
|
||||
resultaat = await KluisService.UpdateAsync(BewerkId, _gebruikerId, Nieuw.Categorie, Nieuw.Titel,
|
||||
payload, Nieuw.Vrijgave, datum, Nieuw.BesprekenMetPersonId);
|
||||
}
|
||||
|
||||
if (resultaat.Succeeded)
|
||||
{
|
||||
Melding = BewerkId == 0 ? "Opgeslagen in je kluis." : "De wijziging is opgeslagen.";
|
||||
MeldingSoort = "goed";
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwItem();
|
||||
Bijlagen = null;
|
||||
ToonFormulier = false;
|
||||
await Verversen();
|
||||
}
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
private static string? Leeg(string? s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
|
||||
|
||||
private string BeheerderNaam(long personId) =>
|
||||
Beheerders?.FirstOrDefault(b => b.PersonId == personId)?.DisplayName ?? "een beheerder";
|
||||
|
||||
// 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 static string Voorbeeld(VaultCategory c) => c switch
|
||||
{
|
||||
VaultCategory.Uitvaartwens => "Mijn uitvaart",
|
||||
VaultCategory.Donorcodicil => "Mijn donorcodicil",
|
||||
VaultCategory.Polis => "Uitvaartverzekering",
|
||||
VaultCategory.Wachtwoord => "Inloggen bij de bank",
|
||||
VaultCategory.Apparaat => "Laptop",
|
||||
VaultCategory.Pincode => "Telefoon",
|
||||
VaultCategory.SocialMedia => "Mijn Facebook",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private static string Vrijgave(VaultItem item) => item.ReleasePolicy switch
|
||||
{
|
||||
ReleasePolicy.NaOverlijden => "vrij na mijn overlijden",
|
||||
ReleasePolicy.OpDatum => $"vrij op {Datums.Datum(item.ReleaseAt)}",
|
||||
_ => "alleen voor mij"
|
||||
};
|
||||
|
||||
public class NieuwItem
|
||||
{
|
||||
public VaultCategory Categorie { get; set; } = VaultCategory.Uitvaartwens;
|
||||
public string Titel { get; set; } = "";
|
||||
public string? Notitie { get; set; }
|
||||
public string? Website { get; set; }
|
||||
public string? Gebruikersnaam { get; set; }
|
||||
public string? Geheim { get; set; }
|
||||
public string? Herstelcodes { get; set; }
|
||||
public string? Platform { get; set; }
|
||||
public string? PlatformAnders { get; set; }
|
||||
public string? Pincode { get; set; }
|
||||
public string? Apparaat { get; set; }
|
||||
public string? Verzekeraar { get; set; }
|
||||
public string? Polisnummer { get; set; }
|
||||
public string? Telefoon { get; set; }
|
||||
public string? BegravenOfCremeren { get; set; }
|
||||
public string? Locatie { get; set; }
|
||||
public string? Muziek { get; set; }
|
||||
public string? Sprekers { get; set; }
|
||||
public string? Wensen { get; set; }
|
||||
public long? BesprekenMetPersonId { get; set; }
|
||||
public ReleasePolicy Vrijgave { get; set; } = ReleasePolicy.NaOverlijden;
|
||||
public DateOnly? VrijgaveDatum { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@page "/leefnog/{Token}"
|
||||
@inject LegacyService Nalatenschap
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
<PageTitle>Ik ben er nog - MijnBesluit</PageTitle>
|
||||
|
||||
@* Bezwaar maken tegen een overlijdensmelding, zonder in te loggen.
|
||||
|
||||
Dat "zonder inloggen" is geen gemak maar noodzaak. Zodra een overlijden is bevestigd staat het
|
||||
account op "overleden" en kun je er niet meer in. Wie dan ten onrechte dood is verklaard, zou
|
||||
zonder deze pagina vastzitten - en juist diegene heeft het het hardst nodig.
|
||||
|
||||
De link uit de e-mail is het bewijs, net als bij een uitnodiging. Hij komt alleen aan op het
|
||||
adres van de betrokkene zelf. *@
|
||||
|
||||
<div class="middenblok">
|
||||
@if (Naam is not null)
|
||||
{
|
||||
<h1>Fijn dat je er nog bent</h1>
|
||||
<div class="melding goed">
|
||||
<p style="margin-top:0">
|
||||
We hebben genoteerd dat @Naam gewoon in leven is. Alle meldingen zijn ingetrokken
|
||||
en niemand heeft toegang gekregen tot wat er in de kluis staat.
|
||||
</p>
|
||||
<p style="margin-bottom:0">
|
||||
Is er eerder al toegang verleend, dan is die weer ingetrokken. Je kunt gewoon
|
||||
weer inloggen.
|
||||
</p>
|
||||
</div>
|
||||
<p><a class="knop" href="/inloggen">Naar het inloggen</a></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1>Ben je er nog?</h1>
|
||||
<p class="zacht">
|
||||
Er is bij MijnBesluit gemeld dat je bent overleden. Klopt dat niet, laat het dan
|
||||
hieronder weten. Alle meldingen vervallen dan, en als er al iemand toegang had
|
||||
gekregen tot je kluis, wordt die weer ingetrokken.
|
||||
</p>
|
||||
|
||||
@if (Fout is not null) { <div class="melding fout">@Fout</div> }
|
||||
|
||||
<EditForm Model="Invoer" FormName="leefnog" OnValidSubmit="Bezwaar" method="post">
|
||||
<div class="kaart">
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Je hoeft hier niet voor in te loggen. Deze link uit je e-mail is genoeg.
|
||||
</p>
|
||||
<button type="submit" class="knop vol">Ik ben er nog</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Token { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Het model van het formulier. Bewust GEEN [SupplyParameterFromForm]: er zijn geen
|
||||
/// invoervelden, er valt niets te binden, en met dat kenmerk erop zonder FormName zet de
|
||||
/// binder het model op null. Zie Uitnodiging.razor, waar dat een harde fout opleverde.
|
||||
/// </summary>
|
||||
private LeegFormulier Invoer { get; } = new();
|
||||
|
||||
/// <summary>De naam van wie er nog is; null zolang er nog niets is bevestigd.</summary>
|
||||
private string? Naam;
|
||||
private string? Fout;
|
||||
|
||||
private async Task Bezwaar()
|
||||
{
|
||||
var resultaat = await Nalatenschap.BezwaarMetTokenAsync(Token);
|
||||
|
||||
if (resultaat.Succeeded) Naam = resultaat.Value;
|
||||
else Fout = resultaat.Error;
|
||||
}
|
||||
|
||||
/// <summary>De link is zelf het bewijs; er hoeft niets ingevuld te worden.</summary>
|
||||
public class LeegFormulier { }
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@page "/moment/{Token}"
|
||||
@using MijnBesluit.Application.Services
|
||||
@inject MessageService Berichten
|
||||
|
||||
@* De landingspagina achter een kijklink: waar iemand zonder MijnBesluit-account een voor hem
|
||||
klaargezet bericht kan bekijken, foto's/video's/documenten inbegrepen, zonder in te loggen.
|
||||
Bewust GEEN [Authorize] en GEEN @rendermode InteractiveServer - dit is een gewone, statisch
|
||||
gerenderde pagina die alleen leest, precies zoals Uitnodiging.razor voor een uitnodigingslink.
|
||||
|
||||
De link zelf is het bewijs: wie hem heeft, mag zien wat erachter zit. Zie
|
||||
MessageService.ViewByTokenAsync voor hoe dat versleuteld blijft tot dit moment. *@
|
||||
|
||||
<PageTitle>Een bericht voor jou - MijnBesluit</PageTitle>
|
||||
|
||||
<div class="middenblok">
|
||||
@if (Fout is not null)
|
||||
{
|
||||
<h1>Deze link werkt niet (meer)</h1>
|
||||
<div class="melding fout">@Fout</div>
|
||||
<p class="zacht" style="margin-top:1rem">
|
||||
Vraag degene die het bericht klaarzette om het opnieuw te versturen. Heb je zelf al
|
||||
een MijnBesluit, log dan in - daar staan al je berichten gewoon bij elkaar.
|
||||
</p>
|
||||
}
|
||||
else if (Weergave is not null)
|
||||
{
|
||||
<h1>@Weergave.Subject</h1>
|
||||
<p class="zacht">Van @Weergave.AfzenderNaam.</p>
|
||||
|
||||
<div class="kaart">
|
||||
<p style="white-space:pre-wrap">@Weergave.Text</p>
|
||||
|
||||
@if (Weergave.VaultFileId is not null)
|
||||
{
|
||||
<div class="rij bijlage" style="margin-top:1rem">
|
||||
@if (IsAfbeelding(Weergave.BestandsType))
|
||||
{
|
||||
<a href="/moment/@Token/bestand" target="_blank" title="Groot bekijken">
|
||||
<img class="bijlage-klein" src="/moment/@Token/bestand" alt="@Weergave.BestandsNaam" />
|
||||
</a>
|
||||
}
|
||||
else if (IsVideo(Weergave.BestandsType))
|
||||
{
|
||||
<video controls style="max-width:100%"><source src="/moment/@Token/bestand" type="@Weergave.BestandsType" /></video>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="teken">📄</span>
|
||||
}
|
||||
<div class="groei">
|
||||
<a href="/moment/@Token/bestand" target="_blank">@Weergave.BestandsNaam</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Weergave.KanNogClaimen)
|
||||
{
|
||||
<p class="zacht" style="margin-top:1rem">
|
||||
Wil je zelf ook een MijnBesluit beginnen - bijvoorbeeld om je eigen berichten aan
|
||||
anderen klaar te zetten? Vraag degene die je toevoegde om een uitnodiging, of
|
||||
<a href="/">kijk eerst rond op de website</a>.
|
||||
</p>
|
||||
}
|
||||
|
||||
<p class="zacht" style="margin-top:1rem">Deze link blijft nog 30 dagen werken vanaf het versturen.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Token { get; set; } = "";
|
||||
|
||||
private MessageService.MomentWeergave? Weergave;
|
||||
private string? Fout;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var resultaat = await Berichten.ViewByTokenAsync(Token);
|
||||
if (resultaat.Succeeded) Weergave = resultaat.Value;
|
||||
else Fout = resultaat.Error;
|
||||
}
|
||||
|
||||
private static bool IsAfbeelding(string? contentType) =>
|
||||
contentType is not null && contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsVideo(string? contentType) =>
|
||||
contentType is not null && contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
@page "/momenten"
|
||||
@attribute [Authorize]
|
||||
@using MijnBesluit.Application.Abstractions
|
||||
@inject MessageService Berichten
|
||||
@inject FamilyService Families
|
||||
@inject IFamilyRepository FamilieOpslag
|
||||
@inject VaultService Kluis
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Momenten - MijnBesluit</PageTitle>
|
||||
|
||||
<h1>Momenten</h1>
|
||||
<p class="zacht">
|
||||
Berichten die voor jou klaarstaan, en berichten die jij hebt klaargezet voor een ander —
|
||||
nu, op een datum, of pas als je er niet meer bent.
|
||||
</p>
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
<div class="kaart">
|
||||
<div class="rij">
|
||||
<button class="knop @(Tab == "in" ? "" : "rustig")" @onclick='() => Tab = "in"'>Voor mij (@(Inkomend?.Count ?? 0))</button>
|
||||
<button class="knop @(Tab == "uit" ? "" : "rustig")" @onclick='() => Tab = "uit"'>Door mij (@(Uitgaand?.Count ?? 0))</button>
|
||||
<div class="groei"></div>
|
||||
@* Zie Kluis.razor: de popup heeft zijn eigen Annuleren, dus hier geen "Sluiten". *@
|
||||
<button class="knop rustig" @onclick="Beginnen">Nieuw bericht</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (ToonFormulier)
|
||||
{
|
||||
@* Zie Kluis.razor: de EditForm staat om de popup heen omdat de invoervelden zijn context
|
||||
nodig hebben, terwijl de knoppen onderin de popup horen. *@
|
||||
<EditForm Model="Nieuw" OnValidSubmit="Versturen" FormName="bericht">
|
||||
<Popup Titel="@(BewerkId == 0 ? "Een bericht klaarzetten" : "Bericht wijzigen")"
|
||||
Bevestigtekst="@(BewerkId == 0 ? "Klaarzetten" : "Wijziging opslaan")"
|
||||
OpBevestigen="Versturen" OpSluiten="Annuleren">
|
||||
<div class="veld">
|
||||
<label for="aan">Voor wie?</label>
|
||||
<InputSelect id="aan" @bind-Value="Nieuw.AanPersoonId">
|
||||
<option value="0">Kies iemand</option>
|
||||
@if (Leden is not null)
|
||||
{
|
||||
@foreach (var lid in Leden.Where(l => l.PersonId != _persoonId))
|
||||
{
|
||||
<option value="@lid.PersonId">@lid.DisplayName</option>
|
||||
}
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="onderwerp">Onderwerp</label>
|
||||
<InputText id="onderwerp" @bind-Value="Nieuw.Onderwerp" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="tekst">Je bericht</label>
|
||||
<InputTextArea id="tekst" @bind-Value="Nieuw.Tekst" />
|
||||
<div class="uitleg">De tekst wordt versleuteld opgeslagen en is pas leesbaar zodra het bericht bezorgd is.</div>
|
||||
</div>
|
||||
|
||||
@if (BewerkId == 0)
|
||||
{
|
||||
<div class="veld">
|
||||
<label>Foto's en documenten</label>
|
||||
<div class="uitleg">
|
||||
Die kun je toevoegen zodra het bericht klaarstaat; ze horen bij het
|
||||
bericht en dat moet er eerst zijn.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Bijlagenblok Bijlagen="Bijlagen" Bezig="@Uploaden"
|
||||
Uitleg="Een foto, een brief die je hebt ingescand, of een filmpje. Het gaat mee met het bericht."
|
||||
OpKiezen="BijlageKiezen"
|
||||
OpVerwijderen="BijlageWeg" />
|
||||
}
|
||||
<div class="veld">
|
||||
<label>Wanneer mag het bezorgd worden?</label>
|
||||
<div class="keuze">
|
||||
<input type="radio" id="b0" name="wanneer" checked="@(Nieuw.Wanneer == "nu")"
|
||||
@onchange='() => Nieuw.Wanneer = "nu"' />
|
||||
<label for="b0">Meteen</label>
|
||||
</div>
|
||||
<div class="keuze">
|
||||
<input type="radio" id="b1" name="wanneer" checked="@(Nieuw.Wanneer == "datum")"
|
||||
@onchange='() => Nieuw.Wanneer = "datum"' />
|
||||
<label for="b1">Op een datum</label>
|
||||
</div>
|
||||
<div class="keuze">
|
||||
<input type="radio" id="b2" name="wanneer" checked="@(Nieuw.Wanneer == "overlijden")"
|
||||
@onchange='() => Nieuw.Wanneer = "overlijden"' />
|
||||
<label for="b2">Pas als ik er niet meer ben</label>
|
||||
</div>
|
||||
@if (Nieuw.Wanneer == "datum")
|
||||
{
|
||||
<InputDate @bind-Value="Nieuw.Datum" />
|
||||
}
|
||||
</div>
|
||||
</Popup>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@if (Tab == "in")
|
||||
{
|
||||
@if (Inkomend is null) { <p class="zacht">Bezig met laden…</p> }
|
||||
else if (Inkomend.Count == 0)
|
||||
{
|
||||
<div class="leeg"><span class="teken">🎬</span> Er staan nog geen berichten voor je klaar.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stapel">
|
||||
@foreach (var bericht in Inkomend)
|
||||
{
|
||||
<div class="kaart">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<div class="groot">@bericht.Subject</div>
|
||||
<div class="zacht">
|
||||
Bezorgd @Datums.Datum(bericht.DeliveredAt)
|
||||
@if (bericht.Status == MessageStatus.Bezorgd) { <span class="label geclaimd">Nieuw</span> }
|
||||
</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => Lezen(bericht.Id)">Lezen</button>
|
||||
</div>
|
||||
|
||||
@if (Verwijderen == bericht.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.9rem">
|
||||
Dit bericht verwijderen? Het verdwijnt dan alleen bij jou; @NaamVan(bericht.FromPersonId)
|
||||
houdt het eigen exemplaar gewoon.
|
||||
<div class="rij" style="margin-top:.7rem">
|
||||
<button class="knop gevaar" @onclick="() => VerwijderenAsync(bericht.Id)">Ja, verwijderen</button>
|
||||
<button class="knop rustig" @onclick="() => Verwijderen = null">Nee, laat staan</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="rij" style="margin-top:.5rem;justify-content:flex-end">
|
||||
<button class="knop rustig klein" title="Verwijderen" aria-label="Verwijderen"
|
||||
@onclick="() => Verwijderen = bericht.Id">🗑️</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Tab == "uit")
|
||||
{
|
||||
@if (Uitgaand is null) { <p class="zacht">Bezig met laden…</p> }
|
||||
else if (Uitgaand.Count == 0)
|
||||
{
|
||||
<div class="leeg"><span class="teken">🎬</span> Je hebt nog niets klaargezet.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stapel">
|
||||
@foreach (var bericht in Uitgaand)
|
||||
{
|
||||
<div class="kaart">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<div class="groot">@bericht.Subject</div>
|
||||
<div class="zacht">Voor @NaamVan(bericht.ToPersonId) · @Bezorging(bericht)</div>
|
||||
</div>
|
||||
<span class="label">@Stand(bericht.Status)</span>
|
||||
</div>
|
||||
|
||||
@if (bericht.Status is MessageStatus.Bezorgd or MessageStatus.Gelezen)
|
||||
{
|
||||
@if (Verwijderen == bericht.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.9rem">
|
||||
Dit bericht verwijderen? Het verdwijnt dan alleen bij jou; @NaamVan(bericht.ToPersonId)
|
||||
houdt het eigen exemplaar gewoon.
|
||||
<div class="rij" style="margin-top:.7rem">
|
||||
<button class="knop gevaar" @onclick="() => VerwijderenAsync(bericht.Id)">Ja, verwijderen</button>
|
||||
<button class="knop rustig" @onclick="() => Verwijderen = null">Nee, laat staan</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="rij" style="margin-top:.9rem;justify-content:flex-end">
|
||||
<button class="knop rustig klein" @onclick="() => BekijkenUit(bericht.Id)">Bekijken</button>
|
||||
<button class="knop rustig klein" title="Verwijderen" aria-label="Verwijderen"
|
||||
@onclick="() => Verwijderen = bericht.Id">🗑️</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (Weghalen == bericht.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.9rem">
|
||||
Dit bericht terugtrekken? Het wordt dan nooit bezorgd.
|
||||
<div class="rij" style="margin-top:.7rem">
|
||||
<button class="knop gevaar" @onclick="() => Intrekken(bericht.Id)">Ja, terugtrekken</button>
|
||||
<button class="knop rustig" @onclick="() => Weghalen = null">Nee, laat staan</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="rij" style="margin-top:.9rem">
|
||||
<button class="knop rustig" @onclick="() => Wijzigen(bericht)">Wijzigen</button>
|
||||
<button class="knop rustig" @onclick="() => Weghalen = bericht.Id">Terugtrekken</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@* Het bericht dat openstaat, in een popup - zoals overal elders op de website. Er stond hier
|
||||
eerder een tekst die onder de tegel zelf uitklapte; dat was de enige plek waar lezen niet in
|
||||
een popup ging. *@
|
||||
@if (Open is { } openId && Inkomend is not null && Inkomend.FirstOrDefault(b => b.Id == openId) is { } openBericht)
|
||||
{
|
||||
<Popup Titel="@openBericht.Subject" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="Dichtdoen" OpSluiten="Dichtdoen">
|
||||
<p class="zacht" style="margin-top:0">Bezorgd @Datums.Datum(openBericht.DeliveredAt)</p>
|
||||
<p style="white-space:pre-wrap;margin:0">@Tekst</p>
|
||||
</Popup>
|
||||
}
|
||||
|
||||
@* Je eigen verzonden bericht ter inzage, in dezelfde opmaak als hierboven. *@
|
||||
@if (OpenUit is { } openUitId && Uitgaand is not null && Uitgaand.FirstOrDefault(b => b.Id == openUitId) is { } openUitBericht)
|
||||
{
|
||||
<Popup Titel="@openUitBericht.Subject" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="DichtdoenUit" OpSluiten="DichtdoenUit">
|
||||
<p class="zacht" style="margin-top:0">Voor @NaamVan(openUitBericht.ToPersonId) · @Bezorging(openUitBericht)</p>
|
||||
|
||||
@* Alleen zolang het bericht al bezorgd is: daarvoor staat er nog niets om te tonen. *@
|
||||
@if (openUitBericht.DeliveredAt is not null)
|
||||
{
|
||||
<p class="zacht" style="margin:.3rem 0 0">
|
||||
Verzonden: @Datums.Datum(openUitBericht.DeliveredAt)<br />
|
||||
Gelezen: @(openUitBericht.ReadAt is { } gelezenOp ? Datums.Datum(gelezenOp) : "nog niet")
|
||||
</p>
|
||||
}
|
||||
|
||||
<p style="white-space:pre-wrap;margin:.8rem 0 0">@TekstUit</p>
|
||||
</Popup>
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private long _gebruikerId, _persoonId, _familieId;
|
||||
private IReadOnlyList<Message>? Inkomend, Uitgaand;
|
||||
private IReadOnlyList<FamilyMemberView>? Leden;
|
||||
private string Tab = "in";
|
||||
private long? Open;
|
||||
private string? Tekst, Melding, MeldingSoort;
|
||||
|
||||
/// <summary>Welk eigen verzonden bericht ter inzage openstaat; los van Open hierboven, dat
|
||||
/// is voor "Voor mij".</summary>
|
||||
private long? OpenUit;
|
||||
private string? TekstUit;
|
||||
private bool ToonFormulier;
|
||||
private NieuwBericht Nieuw = new();
|
||||
|
||||
/// <summary>Het bericht dat bewerkt wordt; 0 betekent een nieuw bericht.</summary>
|
||||
private long BewerkId;
|
||||
|
||||
/// <summary>Het bericht dat op terugtrekken staat te wachten.</summary>
|
||||
private long? Weghalen;
|
||||
|
||||
/// <summary>Welke tegel om bevestiging vraagt om te verwijderen - bij "Voor mij" of bij
|
||||
/// "Van mij", ook als het bericht al bezorgd is (in tegenstelling tot Weghalen hierboven,
|
||||
/// dat alleen voor terugtrekken vóór bezorging is).</summary>
|
||||
private long? Verwijderen;
|
||||
|
||||
/// <summary>De bijlagen bij het bericht dat in het wijzigscherm staat.</summary>
|
||||
private IReadOnlyList<VaultFile> Bijlagen = Array.Empty<VaultFile>();
|
||||
private bool Uploaden;
|
||||
|
||||
/// <summary>Zoveel bestanden tegelijk kiezen mag. Blazor wil hier een bovengrens.</summary>
|
||||
private const int MaxTegelijk = 10;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
var user = (await AuthState).User;
|
||||
_gebruikerId = user.UserId() ?? 0;
|
||||
_persoonId = user.PersonId() ?? 0;
|
||||
|
||||
_familieId = (await FamilieOpslag.ListOwnedByPersonAsync(_persoonId)).FirstOrDefault()?.Id ?? 0;
|
||||
if (_familieId != 0) Leden = await Families.ListMembersAsync(_familieId, _gebruikerId);
|
||||
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Verversen()
|
||||
{
|
||||
Inkomend = await Berichten.InboxAsync(_persoonId);
|
||||
Uitgaand = await Berichten.OutboxAsync(_persoonId);
|
||||
Open = null; Tekst = null;
|
||||
}
|
||||
|
||||
private async Task Lezen(long id)
|
||||
{
|
||||
var resultaat = await Berichten.ReadAsync(id, _gebruikerId);
|
||||
if (resultaat.Succeeded) { Open = id; Tekst = resultaat.Value; }
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
/// <summary>Doet de popup met het gelezen bericht weer dicht.</summary>
|
||||
private void Dichtdoen()
|
||||
{
|
||||
Open = null;
|
||||
Tekst = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Je eigen verzonden bericht nog eens bekijken, ook als het al bezorgd is. Anders dan
|
||||
/// Lezen hierboven (voor "Voor mij", dat meteen ook als gelezen markeert): hier verandert
|
||||
/// er niets aan de status.
|
||||
/// </summary>
|
||||
private async Task BekijkenUit(long id)
|
||||
{
|
||||
var resultaat = await Berichten.ReadSentAsync(id, _gebruikerId);
|
||||
if (resultaat.Succeeded) { OpenUit = id; TekstUit = resultaat.Value; }
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
/// <summary>Doet de popup met het eigen verzonden bericht weer dicht.</summary>
|
||||
private void DichtdoenUit()
|
||||
{
|
||||
OpenUit = null;
|
||||
TekstUit = null;
|
||||
}
|
||||
|
||||
private async Task Wijzigen(Message bericht)
|
||||
{
|
||||
// De tekst staat versleuteld; die halen we op om het formulier te vullen. Let op:
|
||||
// ConceptAsync en niet ReadAsync - dat laatste is voor de ontvanger van een bezorgd bericht.
|
||||
var resultaat = await Berichten.ConceptAsync(bericht.Id, _gebruikerId);
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
Melding = resultaat.Error;
|
||||
MeldingSoort = "fout";
|
||||
return;
|
||||
}
|
||||
|
||||
Nieuw = new NieuwBericht
|
||||
{
|
||||
AanPersoonId = bericht.ToPersonId,
|
||||
Onderwerp = bericht.Subject,
|
||||
Tekst = resultaat.Value ?? "",
|
||||
Wanneer = bericht.DeliverAfterDeath ? "overlijden" : bericht.DeliverAt is null ? "nu" : "datum",
|
||||
Datum = bericht.DeliverAt is { } at ? DateOnly.FromDateTime(at.ToLocalTime()) : null
|
||||
};
|
||||
|
||||
BewerkId = bericht.Id;
|
||||
ToonFormulier = true;
|
||||
Weghalen = null;
|
||||
Tab = "uit";
|
||||
Melding = null;
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ foto's en documenten
|
||||
|
||||
private async Task BijlagenLaden()
|
||||
{
|
||||
if (BewerkId == 0) { Bijlagen = Array.Empty<VaultFile>(); return; }
|
||||
|
||||
var resultaat = await Kluis.ListMessageFilesAsync(BewerkId, _gebruikerId);
|
||||
Bijlagen = resultaat.Succeeded ? resultaat.Value! : Array.Empty<VaultFile>();
|
||||
}
|
||||
|
||||
private async Task BijlageKiezen(InputFileChangeEventArgs e)
|
||||
{
|
||||
if (BewerkId == 0) return;
|
||||
|
||||
Uploaden = true;
|
||||
Melding = null;
|
||||
try
|
||||
{
|
||||
foreach (var bestand in e.GetMultipleFiles(MaxTegelijk))
|
||||
{
|
||||
if (bestand.Size > VaultService.MaxBestandsgrootte)
|
||||
{
|
||||
Melding = $"{bestand.Name} is te groot. Maximaal {VaultService.MaxBestandsgrootte / 1024 / 1024} MB.";
|
||||
MeldingSoort = "fout";
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stroom = bestand.OpenReadStream(VaultService.MaxBestandsgrootte);
|
||||
using var buffer = new MemoryStream();
|
||||
await stroom.CopyToAsync(buffer);
|
||||
|
||||
var resultaat = await Kluis.StoreFileAsync(
|
||||
_gebruikerId, bestand.Name,
|
||||
string.IsNullOrWhiteSpace(bestand.ContentType) ? "application/octet-stream" : bestand.ContentType,
|
||||
buffer.ToArray(), messageId: BewerkId);
|
||||
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melding = $"Het bestand kon niet worden gelezen: {ex.Message}";
|
||||
MeldingSoort = "fout";
|
||||
}
|
||||
finally { Uploaden = false; }
|
||||
}
|
||||
|
||||
private async Task BijlageWeg(long bestandId)
|
||||
{
|
||||
var resultaat = await Kluis.DeleteFileAsync(bestandId, _gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
/// <summary>Opent het formulier met een leeg bericht, ook als er net iets anders openstond.</summary>
|
||||
private void Beginnen()
|
||||
{
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwBericht();
|
||||
Bijlagen = Array.Empty<VaultFile>();
|
||||
ToonFormulier = true;
|
||||
}
|
||||
|
||||
private void Annuleren()
|
||||
{
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwBericht();
|
||||
Bijlagen = Array.Empty<VaultFile>();
|
||||
ToonFormulier = false;
|
||||
}
|
||||
|
||||
private async Task Intrekken(long id)
|
||||
{
|
||||
var resultaat = await Berichten.WithdrawAsync(id, _gebruikerId);
|
||||
Melding = resultaat.Succeeded ? "Het bericht is teruggetrokken." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
Weghalen = null;
|
||||
if (resultaat.Succeeded) await Verversen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Een bericht uit je eigen lijst halen - bij "Voor mij" of bij "Van mij", bezorgd of niet.
|
||||
/// Anders dan Intrekken hierboven (dat een bericht terugtrekt vóór het is bezorgd, zodat de
|
||||
/// ander het nooit krijgt): dit ruimt alleen je eigen exemplaar op, de ander houdt het zijne.
|
||||
/// </summary>
|
||||
private async Task VerwijderenAsync(long id)
|
||||
{
|
||||
var resultaat = await Berichten.DeleteForMeAsync(id, _gebruikerId);
|
||||
Melding = resultaat.Succeeded ? "Het bericht is verwijderd." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
Verwijderen = null;
|
||||
if (resultaat.Succeeded) await Verversen();
|
||||
}
|
||||
|
||||
private async Task Versturen()
|
||||
{
|
||||
if (BewerkId == 0 && Nieuw.AanPersoonId == 0)
|
||||
{
|
||||
Melding = "Kies eerst voor wie het bericht is.";
|
||||
MeldingSoort = "fout";
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime? datum = Nieuw.Wanneer == "datum" && Nieuw.Datum is { } d
|
||||
? d.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc) : null;
|
||||
|
||||
Result resultaat;
|
||||
if (BewerkId == 0)
|
||||
{
|
||||
var gemaakt = await Berichten.ComposeAsync(_gebruikerId, Nieuw.AanPersoonId, Nieuw.Onderwerp,
|
||||
Nieuw.Tekst, datum, Nieuw.Wanneer == "overlijden");
|
||||
resultaat = gemaakt.Succeeded ? Result.Ok() : Result.Fail(gemaakt.Error!);
|
||||
}
|
||||
else
|
||||
{
|
||||
resultaat = await Berichten.UpdateAsync(BewerkId, _gebruikerId, Nieuw.Onderwerp, Nieuw.Tekst,
|
||||
datum, Nieuw.Wanneer == "overlijden");
|
||||
}
|
||||
|
||||
if (resultaat.Succeeded)
|
||||
{
|
||||
Melding = BewerkId == 0 ? "Je bericht staat klaar." : "De wijziging is opgeslagen.";
|
||||
MeldingSoort = "goed";
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwBericht();
|
||||
ToonFormulier = false;
|
||||
await Verversen();
|
||||
}
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
private string NaamVan(long persoonId) =>
|
||||
Leden?.FirstOrDefault(l => l.PersonId == persoonId)?.DisplayName ?? "onbekend";
|
||||
|
||||
private static string Bezorging(Message m) =>
|
||||
m.DeliverAfterDeath ? "na mijn overlijden"
|
||||
: m.DeliverAt is { } at ? $"op {Datums.Datum(at)}"
|
||||
: "meteen";
|
||||
|
||||
private static string Stand(MessageStatus s) => s switch
|
||||
{
|
||||
MessageStatus.Gepland => "Klaargezet",
|
||||
MessageStatus.Bezorgd => "Bezorgd",
|
||||
MessageStatus.Gelezen => "Gelezen",
|
||||
MessageStatus.Ingetrokken => "Ingetrokken",
|
||||
_ => "Concept"
|
||||
};
|
||||
|
||||
public class NieuwBericht
|
||||
{
|
||||
public long AanPersoonId { get; set; }
|
||||
public string Onderwerp { get; set; } = "";
|
||||
public string Tekst { get; set; } = "";
|
||||
public string Wanneer { get; set; } = "overlijden";
|
||||
public DateOnly? Datum { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
@page "/nalatenschap"
|
||||
@attribute [Authorize]
|
||||
@using MijnBesluit.Application.Abstractions
|
||||
@inject LegacyService NalatenschapService
|
||||
@inject FamilyService Families
|
||||
@inject IFamilyRepository FamilieOpslag
|
||||
@inject ILegacyRepository LegacyOpslag
|
||||
@inject IPersonRepository Personen
|
||||
@inject VaultService Kluis
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Nalatenschap - MijnBesluit</PageTitle>
|
||||
|
||||
<h1>Nalatenschap</h1>
|
||||
<p class="zacht">
|
||||
Wie mag straks regelen wat jij hebt vastgelegd? En hoeveel mensen moeten melden dat je er
|
||||
niet meer bent voordat dat gebeurt?
|
||||
</p>
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
@if (Stand.State == DeathProcedureState.Lopend && Stand.Missing == 0)
|
||||
{
|
||||
<div class="melding let-op">
|
||||
<strong>Er is gemeld dat je zou zijn overleden.</strong>
|
||||
@Stand.ActiveDeclarations mensen hebben dat doorgegeven. Klopt het niet, laat het dan
|
||||
weten vóór @Datums.Datum(Stand.ObjectionDeadline).
|
||||
<div style="margin-top:.7rem">
|
||||
<button class="knop" @onclick="Bezwaar">Ik ben er nog</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (Stand.WachtOpBeheerder)
|
||||
{
|
||||
<div class="melding let-op">
|
||||
Er @(Stand.ActiveDeclarations == 1 ? "is één melding" : $"zijn {Stand.ActiveDeclarations} meldingen")
|
||||
binnengekomen over jouw profiel — genoeg in aantal, maar er zit nog geen melding bij
|
||||
van iemand die jij als beheerder hebt aangewezen. Zolang die er niet is gebeurt er niets.
|
||||
<div style="margin-top:.7rem">
|
||||
<button class="knop" @onclick="Bezwaar">Ik ben er nog</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (Stand.State == DeathProcedureState.Lopend)
|
||||
{
|
||||
<div class="melding let-op">
|
||||
Er @(Stand.ActiveDeclarations == 1 ? "is één melding" : $"zijn {Stand.ActiveDeclarations} meldingen")
|
||||
binnengekomen over jouw profiel. Er @(Stand.Missing == 1 ? "is er nog één" : $"zijn er nog {Stand.Missing}")
|
||||
nodig voordat de procedure begint.
|
||||
<div style="margin-top:.7rem">
|
||||
<button class="knop" @onclick="Bezwaar">Ik ben er nog</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ---------------------------------------------------------------- aan mij toevertrouwd *@
|
||||
@if (Toevertrouwd is { Count: > 0 })
|
||||
{
|
||||
<div class="kaart">
|
||||
<h2 style="margin-top:0">Aan mij toevertrouwd</h2>
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Deze mensen hebben jou aangewezen om hun nalatenschap te regelen. Zolang zij er zijn
|
||||
valt er niets in te zien; dat verandert pas als de procedure is afgerond.
|
||||
</p>
|
||||
|
||||
<div class="stapel">
|
||||
@foreach (var nalatenschap in Toevertrouwd)
|
||||
{
|
||||
<div class="kaart compact">
|
||||
@* De naam en de knop op één regel, de toelichting daaronder over de volle
|
||||
breedte. Naast de knop bleef er op een telefoon zo weinig over dat
|
||||
"Overleden 3 maart 2026" over vier regels ging en de naam werd afgekapt. *@
|
||||
<div class="rij persoon">
|
||||
<Penning Naam="@nalatenschap.DisplayName" Bron="@Foto(nalatenschap.PersonId)" />
|
||||
<span class="groot groei" title="@nalatenschap.DisplayName">@nalatenschap.DisplayName</span>
|
||||
</div>
|
||||
|
||||
<div class="zacht" style="margin-top:.3rem">
|
||||
@if (!nalatenschap.Afgerond)
|
||||
{
|
||||
<text>Je bent aangewezen · volgorde @nalatenschap.Volgorde</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (nalatenschap.DateOfDeath is { } overleden)
|
||||
{
|
||||
<text>Overleden @Datums.Datum(overleden) · </text>
|
||||
}
|
||||
<text>@nalatenschap.AantalItems item@(nalatenschap.AantalItems == 1 ? "" : "s") vrijgegeven</text>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Wat deze persoon zelf heeft ingesteld: zo weet je bij leven al waar je op
|
||||
moet rekenen, en kun je het er nog met diegene over hebben. Alleen zolang
|
||||
de procedure nog niet is afgerond - daarna is het geschiedenis, geen
|
||||
vooruitzicht meer. *@
|
||||
@if (!nalatenschap.Afgerond)
|
||||
{
|
||||
<div class="zacht" style="margin-top:.1rem">
|
||||
@nalatenschap.RequiredDeclarations
|
||||
meldingen nodig · @nalatenschap.ObjectionDays dagen bezwaartermijn
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (nalatenschap.Afgerond)
|
||||
{
|
||||
<button class="knop rustig" style="margin-top:.6rem"
|
||||
@onclick="() => Openen(nalatenschap.PersonId)">
|
||||
@(Bekeken == nalatenschap.PersonId ? "Sluiten" : "Bekijken")
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (nalatenschap.Afgerond && Bekeken == nalatenschap.PersonId)
|
||||
{
|
||||
@if (Nagelaten is { Count: 0 })
|
||||
{
|
||||
<p class="zacht" style="margin:.8rem 0 0">
|
||||
Er is niets vrijgegeven. @nalatenschap.DisplayName heeft alles op
|
||||
"alleen voor mijzelf" laten staan; dat blijft ook nu dicht.
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="stapel" style="margin-top:.8rem">
|
||||
@foreach (var item in Nagelaten ?? new List<VaultItem>())
|
||||
{
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ---------------------------------------------------------------- met mij gedeeld *@
|
||||
@if (GedeeldMetMij is { Count: > 0 })
|
||||
{
|
||||
<div class="kaart">
|
||||
<h2 style="margin-top:0">Met mij gedeeld</h2>
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Deze uitvaartwens is met jou gedeeld om alvast te bespreken - je hoeft niet te
|
||||
wachten op een overlijden om hem te lezen.
|
||||
</p>
|
||||
|
||||
<div class="stapel">
|
||||
@foreach (var item in GedeeldMetMij)
|
||||
{
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ---------------------------------------------------------------- item bekijken *@
|
||||
@* Dezelfde opmaak als het invulformulier in de kluis (.veld-blokken onder elkaar), maar
|
||||
zonder invoervelden: dit is een ander item dan het jouwe, dus er valt hier niets te
|
||||
wijzigen - alleen te lezen en, als er iets bij hangt, te bekijken. *@
|
||||
@if (GeopendItem is not null && Inhoud is not null)
|
||||
{
|
||||
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
||||
<div class="stapel">
|
||||
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud))
|
||||
{
|
||||
<div class="veld">
|
||||
<label>@label</label>
|
||||
<p style="margin:.2rem 0 0">@waarde</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (GeopendBijlagen is { Count: > 0 })
|
||||
{
|
||||
<hr />
|
||||
<h3>Bijlagen</h3>
|
||||
<div class="stapel">
|
||||
@foreach (var bijlage in GeopendBijlagen)
|
||||
{
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<a href="/kluis/bestand/@bijlage.Id" target="_blank">@bijlage.FileName</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (bijlage.ContentType.StartsWith("image/"))
|
||||
{
|
||||
<img src="/kluis/bestand/@bijlage.Id" alt="@bijlage.FileName"
|
||||
style="max-width:100%;border-radius:10px;margin:.4rem 0 1rem" />
|
||||
}
|
||||
else if (bijlage.ContentType.StartsWith("video/"))
|
||||
{
|
||||
<video src="/kluis/bestand/@bijlage.Id" controls
|
||||
style="max-width:100%;border-radius:10px;margin:.4rem 0 1rem"></video>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Popup>
|
||||
}
|
||||
|
||||
<div class="kaart">
|
||||
<h2>Mijn beheerders</h2>
|
||||
@if (Beheerders is null) { <p class="zacht">Bezig met laden…</p> }
|
||||
else if (Beheerders.Count == 0)
|
||||
{
|
||||
<p class="zacht">Je hebt nog niemand aangewezen. Zonder beheerder blijft je kluis dicht.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stapel">
|
||||
@foreach (var beheerder in Beheerders)
|
||||
{
|
||||
<div class="rij persoon">
|
||||
<Penning Naam="@NaamVan(beheerder.ManagerPersonId)" Bron="@Foto(beheerder.ManagerPersonId)" />
|
||||
<div class="groei">
|
||||
<div class="groot" title="@NaamVan(beheerder.ManagerPersonId)">@NaamVan(beheerder.ManagerPersonId)</div>
|
||||
<div class="zacht">
|
||||
Volgorde @beheerder.Priority
|
||||
@if (beheerder.AccessGrantedAt is not null) { <text> · heeft toegang gekregen</text> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Wie je aanwijst moet je ook weer kunnen weghalen - als hij zelf overlijdt,
|
||||
of als je het anders wilt regelen. Behalve als hij je kluis al heeft
|
||||
gekregen: dan valt er niets meer terug te nemen. *@
|
||||
@if (beheerder.AccessGrantedAt is null)
|
||||
{
|
||||
<span class="tegelknoppen">
|
||||
<button class="knop rustig klein"
|
||||
title="Niet meer laten beheren" aria-label="Niet meer laten beheren"
|
||||
@onclick="() => Weghalen = beheerder.ManagerPersonId">🗑️</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Weghalen == beheerder.ManagerPersonId)
|
||||
{
|
||||
<div class="melding let-op">
|
||||
<strong>@NaamVan(beheerder.ManagerPersonId)</strong> niet meer laten beheren?
|
||||
Je kluis gaat dan niet meer naar hem of haar. Wijs wel iemand anders aan:
|
||||
zonder beheerder blijft je kluis ook na je overlijden dicht.
|
||||
<div class="rij" style="margin-top:.7rem">
|
||||
<button class="knop gevaar" @onclick="() => BeheerderWeg(beheerder.ManagerPersonId)">
|
||||
Ja, weghalen
|
||||
</button>
|
||||
<button class="knop rustig" @onclick="() => Weghalen = null">Nee, laat staan</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<hr />
|
||||
<EditForm Model="Nieuw" OnValidSubmit="Aanwijzen" FormName="beheerder">
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label for="wie">Wie wijs je aan?</label>
|
||||
<InputSelect id="wie" @bind-Value="Nieuw.PersoonId">
|
||||
<option value="0">Kies iemand uit je familie</option>
|
||||
@if (Leden is not null)
|
||||
{
|
||||
@foreach (var lid in Leden.Where(l => l.PersonId != _persoonId))
|
||||
{
|
||||
<option value="@lid.PersonId">@lid.DisplayName</option>
|
||||
}
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="volgorde">Volgorde</label>
|
||||
<InputNumber id="volgorde" @bind-Value="Nieuw.Volgorde" />
|
||||
<div class="uitleg">1 is de eerste keus.</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="knop">Aanwijzen</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<div class="kaart">
|
||||
<h2>Hoeveel meldingen zijn er nodig?</h2>
|
||||
<p class="zacht">
|
||||
Standaard moeten drie verschillende mensen melden dat je bent overleden. Daarna krijg je
|
||||
zelf nog een termijn om dat tegen te spreken. Pas als die verstrijkt, krijgt je beheerder toegang.
|
||||
</p>
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label for="aantal">Aantal meldingen</label>
|
||||
<input id="aantal" type="number" min="1" max="10" value="@Aantal" @onchange="AantalGewijzigd" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="dagen">Bezwaartermijn in dagen</label>
|
||||
<input id="dagen" type="number" min="1" max="90" value="@Dagen" @onchange="DagenGewijzigd" />
|
||||
</div>
|
||||
</div>
|
||||
@* Zonder beheerder komt de procedure niet op gang, hoeveel mensen er ook melden. Dat hoort
|
||||
hier te staan en niet pas als het zover is. *@
|
||||
@if (Beheerders is { Count: 0 })
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.8rem">
|
||||
Je hebt nog niemand aangewezen. Melden mag iedereen uit Mijn mensen, maar de procedure
|
||||
begint pas te lopen als er een melding van een van jouw beheerders bij zit. Zonder
|
||||
beheerder gebeurt er dus niets — en blijft je kluis ook na je overlijden dicht.
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="knop rustig" @onclick="InstellingenOpslaan">Opslaan</button>
|
||||
</div>
|
||||
|
||||
@* Melden mag over iedereen uit je eigen mensen. Staat daar niemand in, dan staat dit blok er
|
||||
niet: een knop met een lege lijst is geen knop. *@
|
||||
@if (Meldbaar.Count > 0)
|
||||
{
|
||||
<div class="kaart">
|
||||
<h2>Iemand als overleden melden</h2>
|
||||
<p class="zacht">
|
||||
Is iemand uit Mijn mensen overleden? Geef het hier door. Er zijn meerdere
|
||||
onafhankelijke meldingen nodig voordat er iets gebeurt, waaronder één van iemand die
|
||||
hij zelf als beheerder heeft aangewezen. Daarna krijgt de betrokkene eerst bericht.
|
||||
</p>
|
||||
<EditForm Model="Overlijden" OnValidSubmit="Melden" FormName="overlijden">
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label for="overleden">Wie?</label>
|
||||
<InputSelect id="overleden" @bind-Value="Overlijden.PersoonId">
|
||||
<option value="0">Kies iemand</option>
|
||||
@foreach (var wie in Meldbaar)
|
||||
{
|
||||
<option value="@wie.PersonId">@wie.DisplayName</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="datum">Datum van overlijden</label>
|
||||
<InputDate id="datum" @bind-Value="Overlijden.Datum" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="knop gevaar">Melding doen</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private long _gebruikerId, _persoonId, _familieId;
|
||||
private IReadOnlyList<LegacyManager>? Beheerders;
|
||||
private IReadOnlyList<FamilyMemberView>? Leden;
|
||||
private DeathProcedureResult Stand;
|
||||
private int Aantal = 3, Dagen = 14;
|
||||
private string? Melding, MeldingSoort;
|
||||
private NieuweBeheerder Nieuw = new();
|
||||
private Overlijdensmelding Overlijden = new();
|
||||
|
||||
/// <summary>De nalatenschappen die aan mij zijn toevertrouwd, afgerond of nog niet.</summary>
|
||||
private IReadOnlyList<LegacyService.ToevertrouwdView>? Toevertrouwd;
|
||||
|
||||
/// <summary>Kluisitems die iemand rechtstreeks met mij heeft gedeeld (nog geen overlijden nodig).</summary>
|
||||
private IReadOnlyList<VaultItem>? GedeeldMetMij;
|
||||
|
||||
/// <summary>Per persoon het nummer van zijn profielfoto, voor wie er een heeft.</summary>
|
||||
private IReadOnlyDictionary<long, long> Profielfotos = new Dictionary<long, long>();
|
||||
|
||||
/// <summary>
|
||||
/// Over wie jij een overlijdensmelding mag doen: iedereen uit je eigen mensen die nog niet
|
||||
/// als overleden staat. Dezelfde kring die de server aanhoudt, zodat het scherm en de regel
|
||||
/// het over dezelfde mensen hebben.
|
||||
/// </summary>
|
||||
private IReadOnlyList<FamilyMemberView> Meldbaar =>
|
||||
Leden?.Where(l => l.PersonId != _persoonId && l.DateOfDeath is null).ToList()
|
||||
?? (IReadOnlyList<FamilyMemberView>)Array.Empty<FamilyMemberView>();
|
||||
|
||||
/// <summary>Welke nalatenschap openstaat, en welk item daarbinnen.</summary>
|
||||
private long? Bekeken, GeopendItem;
|
||||
private IReadOnlyList<VaultItem>? Nagelaten;
|
||||
private VaultPayload? Inhoud;
|
||||
private string? GeopendTitel;
|
||||
private IReadOnlyList<VaultFile>? GeopendBijlagen;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
var user = (await AuthState).User;
|
||||
_gebruikerId = user.UserId() ?? 0;
|
||||
_persoonId = user.PersonId() ?? 0;
|
||||
|
||||
_familieId = (await FamilieOpslag.ListOwnedByPersonAsync(_persoonId)).FirstOrDefault()?.Id ?? 0;
|
||||
if (_familieId != 0) Leden = await Families.ListMembersAsync(_familieId, _gebruikerId);
|
||||
|
||||
var ik = await Personen.FindByIdAsync(_persoonId);
|
||||
if (ik is not null) { Aantal = ik.RequiredDeathDeclarations; Dagen = ik.DeathObjectionDays; }
|
||||
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Verversen()
|
||||
{
|
||||
Beheerders = await LegacyOpslag.ListManagersForAsync(_persoonId);
|
||||
Stand = await NalatenschapService.StatusAsync(_persoonId);
|
||||
Toevertrouwd = await NalatenschapService.ToevertrouwdAsync(_gebruikerId);
|
||||
|
||||
var gedeeld = await Kluis.ListSharedWithMeAsync(_gebruikerId);
|
||||
GedeeldMetMij = gedeeld.Succeeded ? gedeeld.Value : Array.Empty<VaultItem>();
|
||||
|
||||
// De profielfoto's van iedereen die op dit scherm staat, in één vraag. Alleen het
|
||||
// nummer; het plaatje zelf haalt de browser op via /bestand/{id}/klein.
|
||||
var wie = Beheerders.Select(b => b.ManagerPersonId)
|
||||
.Concat(Toevertrouwd.Select(t => t.PersonId))
|
||||
.Distinct();
|
||||
|
||||
Profielfotos = (await Kluis.ProfielfotosAsync(wie, _gebruikerId))
|
||||
.ToDictionary(f => f.PersoonId, f => f.BestandId);
|
||||
}
|
||||
|
||||
/// <summary>Waar de profielfoto van deze persoon staat; null als hij er geen heeft.</summary>
|
||||
private string? Foto(long persoonId) =>
|
||||
Profielfotos.TryGetValue(persoonId, out var bestandId) ? $"/bestand/{bestandId}/klein" : null;
|
||||
|
||||
// ------------------------------------------------------------------ aan mij toevertrouwd
|
||||
|
||||
private async Task Openen(long persoonId)
|
||||
{
|
||||
if (Bekeken == persoonId) { Sluiten(); return; }
|
||||
|
||||
var resultaat = await NalatenschapService.NalatenschapItemsAsync(persoonId, _gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; return; }
|
||||
|
||||
Bekeken = persoonId;
|
||||
Nagelaten = resultaat.Value;
|
||||
ItemDichtdoen();
|
||||
}
|
||||
|
||||
private void Sluiten()
|
||||
{
|
||||
Bekeken = null;
|
||||
Nagelaten = null;
|
||||
ItemDichtdoen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opent één nagelaten item. De sleutel daarvoor is bij het bevestigen van het overlijden
|
||||
/// voor jou klaargezet; VaultService gebruikt die vanzelf als het item niet van jou is.
|
||||
/// </summary>
|
||||
private async Task ItemOpenen(VaultItem item)
|
||||
{
|
||||
if (GeopendItem == item.Id) { ItemDichtdoen(); return; }
|
||||
|
||||
var resultaat = await Kluis.OpenAsync(item.Id, _gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; return; }
|
||||
|
||||
GeopendItem = item.Id;
|
||||
Inhoud = resultaat.Value;
|
||||
GeopendTitel = item.Title;
|
||||
|
||||
// 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);
|
||||
GeopendBijlagen = bijlagen.Succeeded ? bijlagen.Value : Array.Empty<VaultFile>();
|
||||
}
|
||||
|
||||
private void ItemDichtdoen()
|
||||
{
|
||||
GeopendItem = null;
|
||||
Inhoud = null;
|
||||
GeopendTitel = null;
|
||||
GeopendBijlagen = null;
|
||||
}
|
||||
|
||||
/// <summary>Van wie de vraag "weghalen?" openstaat.</summary>
|
||||
private long? Weghalen;
|
||||
|
||||
private async Task BeheerderWeg(long persoonId)
|
||||
{
|
||||
var resultaat = await NalatenschapService.RemoveManagerAsync(_gebruikerId, persoonId);
|
||||
|
||||
Melding = resultaat.Succeeded
|
||||
? $"{NaamVan(persoonId)} beheert je nalatenschap niet meer."
|
||||
: resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
|
||||
Weghalen = null;
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Aanwijzen()
|
||||
{
|
||||
if (Nieuw.PersoonId == 0) { Melding = "Kies eerst iemand."; MeldingSoort = "fout"; return; }
|
||||
var resultaat = await NalatenschapService.AppointManagerAsync(_gebruikerId, Nieuw.PersoonId, Nieuw.Volgorde);
|
||||
Melding = resultaat.Succeeded ? "Aangewezen." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
Nieuw = new NieuweBeheerder();
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Bezwaar()
|
||||
{
|
||||
var resultaat = await NalatenschapService.ObjectAsync(_persoonId, _gebruikerId);
|
||||
Melding = resultaat.Succeeded
|
||||
? "Goed dat je het laat weten. De meldingen zijn ingetrokken."
|
||||
: resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Melden()
|
||||
{
|
||||
if (Overlijden.PersoonId == 0) { Melding = "Kies eerst iemand."; MeldingSoort = "fout"; return; }
|
||||
var resultaat = await NalatenschapService.DeclareDeathAsync(Overlijden.PersoonId, _gebruikerId, Overlijden.Datum);
|
||||
if (resultaat.Succeeded)
|
||||
{
|
||||
var stand = resultaat.Value;
|
||||
Melding = stand.Missing > 0
|
||||
? $"Je melding is genoteerd. Er zijn nog {stand.Missing} melding(en) nodig."
|
||||
: "Je melding is genoteerd. De bezwaartermijn is ingegaan.";
|
||||
MeldingSoort = "goed";
|
||||
Overlijden = new Overlijdensmelding();
|
||||
}
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private void AantalGewijzigd(ChangeEventArgs e)
|
||||
{ if (int.TryParse(e.Value?.ToString(), out var v)) Aantal = Math.Clamp(v, 1, 10); }
|
||||
|
||||
private void DagenGewijzigd(ChangeEventArgs e)
|
||||
{ if (int.TryParse(e.Value?.ToString(), out var v)) Dagen = Math.Clamp(v, 0, 90); }
|
||||
|
||||
private async Task InstellingenOpslaan()
|
||||
{
|
||||
var resultaat = await Families.UpdatePersonAsync(_persoonId, _gebruikerId, p =>
|
||||
{
|
||||
p.RequiredDeathDeclarations = Aantal;
|
||||
p.DeathObjectionDays = Dagen;
|
||||
});
|
||||
Melding = resultaat.Succeeded ? "Je instellingen zijn opgeslagen." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
}
|
||||
|
||||
private string NaamVan(long persoonId) =>
|
||||
Leden?.FirstOrDefault(l => l.PersonId == persoonId)?.DisplayName ?? "onbekend";
|
||||
|
||||
public class NieuweBeheerder { public long PersoonId { get; set; } public int Volgorde { get; set; } = 1; }
|
||||
public class Overlijdensmelding { public long PersoonId { get; set; } public DateOnly? Datum { get; set; } }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@page "/privacy"
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
@* De privacyverklaring. Openbaar bereikbaar, want Google Play vraagt er een adres van, en wie
|
||||
overweegt zijn wachtwoorden hier neer te leggen hoort het te kunnen lezen vóórdat hij een
|
||||
account maakt.
|
||||
|
||||
Let op bij het aanpassen: wat hier staat moet waar zijn. Verandert er iets aan wat er wordt
|
||||
opgeslagen, wie het kan zien of wat er naar buiten gaat, dan verandert deze pagina mee. Een
|
||||
privacyverklaring die niet klopt is erger dan geen. *@
|
||||
|
||||
<PageTitle>Privacy - MijnBesluit</PageTitle>
|
||||
|
||||
<div class="middenblok" style="max-width:640px">
|
||||
<h1>Privacy</h1>
|
||||
|
||||
<Privacytekst />
|
||||
|
||||
<p class="zacht"><a href="/">Terug naar MijnBesluit</a></p>
|
||||
</div>
|
||||
@@ -0,0 +1,368 @@
|
||||
@page "/profiel"
|
||||
@attribute [Authorize]
|
||||
@inject AccountService Accounts
|
||||
@inject VaultService Kluis
|
||||
@inject MeldingService Meldingen
|
||||
@inject NavigationManager Navigatie
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Mijn profiel - MijnBesluit</PageTitle>
|
||||
|
||||
@* Je eigen gegevens. Dit is het enige profiel dat niet bij Mijn mensen staat - daar staat
|
||||
iedereen behalve jij - dus zonder deze pagina kon je je eigen naam of adres nergens kwijt.
|
||||
|
||||
Het e-mailadres staat bewust apart van de rest. Dat is je inlog: wie dat zomaar kan
|
||||
verzetten, kan zichzelf buitensluiten of een ander account overnemen. Daarom gaat er eerst
|
||||
een code naar het nieuwe adres. *@
|
||||
|
||||
@* Net als overal elders op de site: aantikken van je eigen kaart bij Mijn mensen opent dit als
|
||||
popup, niet als eigen pagina. "Vast onderin" (.knop-vast-onder stond hier eerder) is precies
|
||||
wat een Popup toch al gratis geeft via .popup-knoppen. De route /profiel blijft bestaan - de
|
||||
kop en het menu linken er nog naar - maar toont nu meteen deze popup. *@
|
||||
@if (Melding is not null && Mijn is null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
@if (Mijn is null)
|
||||
{
|
||||
<p class="zacht">Bezig met laden…</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Popup Titel="Mijn profiel" Bevestigtekst="Opslaan" Bezig="@Bezig"
|
||||
OpBevestigen="Opslaan" OpSluiten="Sluiten">
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
@if (Gewisseld is not null)
|
||||
{
|
||||
<div class="melding goed">
|
||||
<p style="margin-top:0">
|
||||
Je e-mailadres is gewijzigd. Vanaf nu log je in met <strong>@Gewisseld</strong>.
|
||||
Je huidige aanmelding loopt nog op je oude adres, dus log even opnieuw in.
|
||||
</p>
|
||||
|
||||
@* Uitloggen gaat via een formulier en niet vanuit deze component: het cookie kan alleen
|
||||
gewist worden terwijl het antwoord nog niet onderweg is. Zie Program.cs. *@
|
||||
<form method="post" action="/uitloggen" style="margin:0">
|
||||
<AntiforgeryToken />
|
||||
<button type="submit" class="knop">Uitloggen en opnieuw inloggen</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="raster twee">
|
||||
<div class="veld"><label for="vn">Voornaam</label><input id="vn" @bind="Voornaam" /></div>
|
||||
<div class="veld"><label for="an">Achternaam</label><input id="an" @bind="Achternaam" /></div>
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
<label for="gb">Geboortedatum</label>
|
||||
<input id="gb" type="date" max="@DateTime.Today.ToString("yyyy-MM-dd")" @bind="Geboortedatum" />
|
||||
<div class="uitleg">Niet verplicht.</div>
|
||||
</div>
|
||||
|
||||
<p class="zacht" style="margin:.8rem 0 0">Je bent hier sinds @Datum(Mijn.LidSinds).</p>
|
||||
|
||||
<div class="kaart" style="margin-top:1rem">
|
||||
<h2 style="margin-top:0">E-mailadres</h2>
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Hiermee log je in. Je huidige adres is <strong>@Mijn.Email</strong>.
|
||||
</p>
|
||||
|
||||
<button class="knop rustig" disabled="@Bezig" @onclick="AdresWijzigen">
|
||||
✉ Ander e-mailadres
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ---------------------------------------------------------------- meldingen *@
|
||||
<div class="kaart" style="margin-top:1rem">
|
||||
<h2 style="margin-top:0">Meldingen op je telefoon</h2>
|
||||
|
||||
@if (Toestellen.Count == 0)
|
||||
{
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Er is nog geen telefoon aangemeld. Installeer de app, log in en geef toestemming
|
||||
voor meldingen; daarna staat je toestel hier.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="zacht" style="margin-top:0">Meldingen gaan naar:</p>
|
||||
<ul style="margin:0 0 1rem">
|
||||
@foreach (var toestel in Toestellen)
|
||||
{
|
||||
<li>
|
||||
@(string.IsNullOrWhiteSpace(toestel.Naam) ? toestel.Platform : toestel.Naam)
|
||||
<span class="zacht">· laatst gezien @Datum(toestel.LaatstGezien)</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (Proef is not null)
|
||||
{
|
||||
<div class="melding @(Proef.Gelukt ? "goed" : "let-op")">@Proef.Uitleg</div>
|
||||
}
|
||||
|
||||
<button class="knop rustig" disabled="@Bezig" @onclick="ProefmeldingAsync">
|
||||
🔔 Stuur een proefmelding
|
||||
</button>
|
||||
<div class="uitleg">
|
||||
Komt hij niet aan, dan zegt deze knop waar het op vastloopt.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kaart" style="margin-top:1rem">
|
||||
<h2 style="margin-top:0">Mijn foto's en documenten</h2>
|
||||
<Bijlagenblok Titel="Bestanden bij mijn profiel" Bijlagen="Bijlagen" Bezig="@Uploaden"
|
||||
KanProfielfoto="true"
|
||||
Uitleg="Een foto van jezelf, je paspoort, je donorcodicil. Met het sterretje maak je er je profielfoto van."
|
||||
OpKiezen="BijlageKiezen"
|
||||
OpVerwijderen="BijlageWeg"
|
||||
OpProfielfoto="Profielfoto" />
|
||||
</div>
|
||||
|
||||
@* Een link en geen knop: het opheffen zelf gebeurt op een eigen pagina. Die pagina wordt
|
||||
statisch weergegeven, en dat is nodig ook - er wordt een inlogcookie gewist, en dat kan
|
||||
niet tijdens het renderen van een interactief scherm. Diezelfde pagina is ook het
|
||||
openbare adres waar iemand zonder app terechtkan; zie AccountVerwijderen.razor. *@
|
||||
<div class="kaart" style="margin-top:1rem">
|
||||
<h2 style="margin-top:0">Je MijnBesluit opheffen</h2>
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Je kluis, je bestanden en de berichten die nog klaarstaan worden verwijderd. Dat is
|
||||
niet terug te draaien.
|
||||
</p>
|
||||
<a class="knop gevaar" href="/account-verwijderen">Mijn account opheffen</a>
|
||||
</div>
|
||||
</Popup>
|
||||
}
|
||||
|
||||
@* ---------------------------------------------------------------- e-mailadres wijzigen *@
|
||||
@if (Adres is not null)
|
||||
{
|
||||
<Popup Titel="Ander e-mailadres"
|
||||
Bevestigtekst="@(Adres.CodeGevraagd ? "Bevestigen" : "Stuur me een code")"
|
||||
Bezig="@Bezig" OpBevestigen="AdresStap" OpSluiten="() => Adres = null">
|
||||
|
||||
@if (!Adres.CodeGevraagd)
|
||||
{
|
||||
<div class="veld">
|
||||
<label for="nieuw">Je nieuwe e-mailadres</label>
|
||||
<input id="nieuw" type="email" @bind="Adres.Nieuw" />
|
||||
<div class="uitleg">
|
||||
We sturen er een code naartoe. Pas als je die invult verandert je adres,
|
||||
dus een tikfout kan je hier niet buitensluiten.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Er is een code gestuurd naar <strong>@Adres.Nieuw</strong>.</p>
|
||||
|
||||
<div class="veld">
|
||||
<label for="code">De code uit die e-mail</label>
|
||||
<input id="code" inputmode="numeric" maxlength="6" @bind="Adres.Code" />
|
||||
</div>
|
||||
}
|
||||
</Popup>
|
||||
}
|
||||
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
|
||||
private long _gebruikerId;
|
||||
private EigenProfiel? Mijn;
|
||||
|
||||
private string Voornaam = "";
|
||||
private string Achternaam = "";
|
||||
private DateTime? Geboortedatum;
|
||||
|
||||
private string? Melding, MeldingSoort;
|
||||
private bool Bezig;
|
||||
|
||||
/// <summary>Het adreswijzigingsformulier; null als het niet openstaat.</summary>
|
||||
private Adreswissel? Adres;
|
||||
|
||||
/// <summary>Het nieuwe adres, zodra de wijziging rond is. Dan volgt alleen nog uitloggen.</summary>
|
||||
private string? Gewisseld;
|
||||
|
||||
private IReadOnlyList<VaultFile> Bijlagen = Array.Empty<VaultFile>();
|
||||
private bool Uploaden;
|
||||
|
||||
/// <summary>De toestellen waar meldingen heen gaan, en de uitkomst van een proefmelding.</summary>
|
||||
private IReadOnlyList<Toestel> Toestellen = Array.Empty<Toestel>();
|
||||
private MeldingService.Proefuitslag? Proef;
|
||||
|
||||
/// <summary>Zoveel bestanden tegelijk kiezen mag. Blazor wil hier een bovengrens.</summary>
|
||||
private const int MaxTegelijk = 10;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is null) return;
|
||||
_gebruikerId = (await AuthState).User.UserId() ?? 0;
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
private async Task Verversen()
|
||||
{
|
||||
var resultaat = await Accounts.EigenProfielAsync(_gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; return; }
|
||||
|
||||
Mijn = resultaat.Value;
|
||||
Voornaam = Mijn!.Voornaam;
|
||||
Achternaam = Mijn.Achternaam;
|
||||
Geboortedatum = Mijn.Geboortedatum?.ToDateTime(TimeOnly.MinValue);
|
||||
|
||||
Toestellen = (await Meldingen.ToestellenAsync(_gebruikerId)).Where(t => t.IsActief).ToList();
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stuurt een proefmelding naar je eigen telefoon. Blijft er een melding uit, dan zijn er
|
||||
/// drie mogelijke oorzaken die er van buiten hetzelfde uitzien; deze knop zegt welke het is.
|
||||
/// </summary>
|
||||
private async Task ProefmeldingAsync()
|
||||
{
|
||||
Bezig = true;
|
||||
Melding = null;
|
||||
Proef = null;
|
||||
try
|
||||
{
|
||||
Proef = await Meldingen.ProefAsync(_gebruikerId);
|
||||
|
||||
// Een verlopen kenmerk wordt bij het versturen afgemeld; dan klopt de lijst niet meer.
|
||||
Toestellen = (await Meldingen.ToestellenAsync(_gebruikerId)).Where(t => t.IsActief).ToList();
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task Opslaan()
|
||||
{
|
||||
Bezig = true;
|
||||
Melding = null;
|
||||
try
|
||||
{
|
||||
var geboren = Geboortedatum is { } g ? DateOnly.FromDateTime(g) : (DateOnly?)null;
|
||||
var resultaat = await Accounts.WijzigEigenProfielAsync(
|
||||
_gebruikerId, Voornaam.Trim(), Achternaam.Trim(), geboren);
|
||||
|
||||
Melding = resultaat.Succeeded ? "Je gegevens zijn opgeslagen." : resultaat.Error;
|
||||
MeldingSoort = resultaat.Succeeded ? "goed" : "fout";
|
||||
if (resultaat.Succeeded) await Verversen();
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
/// <summary>Annuleren, of de popup gewoon dichttikken: terug naar waar de tegel vandaan kwam.</summary>
|
||||
private void Sluiten() => Navigatie.NavigateTo("/familie");
|
||||
|
||||
// ------------------------------------------------------------------ e-mailadres
|
||||
|
||||
private void AdresWijzigen()
|
||||
{
|
||||
Melding = null;
|
||||
Adres = new Adreswissel();
|
||||
}
|
||||
|
||||
/// <summary>Eén knop, twee stappen: eerst de code aanvragen, daarna hem bevestigen.</summary>
|
||||
private async Task AdresStap()
|
||||
{
|
||||
var wissel = Adres!;
|
||||
Bezig = true;
|
||||
Melding = null;
|
||||
try
|
||||
{
|
||||
if (!wissel.CodeGevraagd)
|
||||
{
|
||||
var gevraagd = await Accounts.VraagAdreswijzigingAanAsync(_gebruikerId, wissel.Nieuw.Trim());
|
||||
if (!gevraagd.Succeeded) { Melding = gevraagd.Error; MeldingSoort = "fout"; return; }
|
||||
|
||||
wissel.Nieuw = wissel.Nieuw.Trim().ToLowerInvariant();
|
||||
wissel.CodeGevraagd = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var bevestigd = await Accounts.BevestigAdreswijzigingAsync(
|
||||
_gebruikerId, wissel.Nieuw, wissel.Code.Trim());
|
||||
if (!bevestigd.Succeeded) { Melding = bevestigd.Error; MeldingSoort = "fout"; return; }
|
||||
|
||||
Gewisseld = wissel.Nieuw;
|
||||
Adres = null;
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ foto's en documenten
|
||||
|
||||
private async Task BijlagenLaden()
|
||||
{
|
||||
if (Mijn is null) { Bijlagen = Array.Empty<VaultFile>(); return; }
|
||||
|
||||
var resultaat = await Kluis.ListPersonFilesAsync(Mijn.PersoonId, _gebruikerId);
|
||||
Bijlagen = resultaat.Succeeded ? resultaat.Value! : Array.Empty<VaultFile>();
|
||||
}
|
||||
|
||||
private async Task BijlageKiezen(InputFileChangeEventArgs e)
|
||||
{
|
||||
if (Mijn is null) return;
|
||||
|
||||
Uploaden = true;
|
||||
Melding = null;
|
||||
try
|
||||
{
|
||||
foreach (var bestand in e.GetMultipleFiles(MaxTegelijk))
|
||||
{
|
||||
if (bestand.Size > VaultService.MaxBestandsgrootte)
|
||||
{
|
||||
Melding = $"{bestand.Name} is te groot. Maximaal {VaultService.MaxBestandsgrootte / 1024 / 1024} MB.";
|
||||
MeldingSoort = "fout";
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stroom = bestand.OpenReadStream(VaultService.MaxBestandsgrootte);
|
||||
using var buffer = new MemoryStream();
|
||||
await stroom.CopyToAsync(buffer);
|
||||
|
||||
var resultaat = await Kluis.StoreFileAsync(
|
||||
_gebruikerId, bestand.Name,
|
||||
string.IsNullOrWhiteSpace(bestand.ContentType) ? "application/octet-stream" : bestand.ContentType,
|
||||
buffer.ToArray(), subjectPersonId: Mijn.PersoonId);
|
||||
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
}
|
||||
|
||||
await BijlagenLaden();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melding = $"Het bestand kon niet worden gelezen: {ex.Message}";
|
||||
MeldingSoort = "fout";
|
||||
}
|
||||
finally { Uploaden = false; }
|
||||
}
|
||||
|
||||
private async Task BijlageWeg(long bestandId)
|
||||
{
|
||||
var resultaat = await Kluis.DeleteFileAsync(bestandId, _gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
private async Task Profielfoto(long bestandId)
|
||||
{
|
||||
var resultaat = await Kluis.SetProfilePhotoAsync(bestandId, _gebruikerId);
|
||||
if (!resultaat.Succeeded) { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
await BijlagenLaden();
|
||||
}
|
||||
|
||||
private static string Datum(DateTime moment) => Datums.Datum(moment);
|
||||
|
||||
/// <summary>Het adreswijzigingsformulier: eerst een adres, daarna de code die daarheen ging.</summary>
|
||||
private class Adreswissel
|
||||
{
|
||||
public string Nieuw { get; set; } = "";
|
||||
public string Code { get; set; } = "";
|
||||
public bool CodeGevraagd { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
@page "/registreren"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject AccountService Accounts
|
||||
@inject IHttpContextAccessor HttpContexten
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
@* Zelfde opzet als de inlogpagina: alle formulieren worden altijd gerenderd en alleen
|
||||
verborgen. Bij een statische pagina is elke POST een nieuwe pagina-instantie, dus een
|
||||
formulier dat op dat moment niet gerenderd wordt, bestaat voor de server niet. *@
|
||||
|
||||
<PageTitle>Account aanmaken - MijnBesluit</PageTitle>
|
||||
|
||||
<div class="middenblok">
|
||||
<h1>Begin je eigen MijnBesluit</h1>
|
||||
<p class="zacht">
|
||||
Je legt vast wat je wilt regelen, en zet berichten klaar voor de mensen om je heen.
|
||||
Daarna nodig je je familie uit.
|
||||
</p>
|
||||
|
||||
@if (Fout is not null) { <div class="melding fout">@Fout</div> }
|
||||
@if (Gelukt is not null) { <div class="melding goed">@Gelukt</div> }
|
||||
|
||||
<div hidden="@ToonCodeveld">
|
||||
<EditForm Model="Aanvraag" FormName="registratiecode" OnValidSubmit="CodeAanvragen" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label for="voornaam">Voornaam</label>
|
||||
<InputText id="voornaam" @bind-Value="Aanvraag.Voornaam" autocomplete="given-name" />
|
||||
<ValidationMessage For="() => Aanvraag.Voornaam" class="uitleg" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="achternaam">Achternaam</label>
|
||||
<InputText id="achternaam" @bind-Value="Aanvraag.Achternaam" autocomplete="family-name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="email">E-mailadres</label>
|
||||
<InputText id="email" @bind-Value="Aanvraag.Email" type="email"
|
||||
autocomplete="username" inputmode="email" placeholder="alles in kleine letters" />
|
||||
<ValidationMessage For="() => Aanvraag.Email" class="uitleg" />
|
||||
</div>
|
||||
|
||||
<details class="uitklap">
|
||||
<summary>Waarom vragen jullie geen wachtwoord?</summary>
|
||||
<p>
|
||||
Bij MijnBesluit log je in met een code die we per e-mail sturen. Er is dus niets
|
||||
te onthouden en niets dat op een andere website gelekt kan worden. Zorg er wel
|
||||
voor dat je e-mail goed beveiligd is, want die is vanaf nu je sleutel.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<button type="submit" class="knop vol">Code aanvragen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<div hidden="@(!ToonCodeveld)">
|
||||
<EditForm Model="Bevestig" FormName="registreren" OnValidSubmit="Aanmaken" method="post">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="kaart">
|
||||
<div class="veld">
|
||||
<label for="email2">E-mailadres</label>
|
||||
<InputText id="email2" @bind-Value="Bevestig.Email" type="email" readonly />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="code">Code</label>
|
||||
<InputText id="code" @bind-Value="Bevestig.Code" class="codeveld" inputmode="numeric"
|
||||
autocomplete="one-time-code" maxlength="6" />
|
||||
<div class="uitleg">
|
||||
Vul hier de code in die wij hebben gestuurd naar
|
||||
<span class="naar-adres">@AfgeschermdAdres</span>.
|
||||
</div>
|
||||
<ValidationMessage For="() => Bevestig.Code" class="uitleg" />
|
||||
</div>
|
||||
<button type="submit" class="knop vol">Account aanmaken</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
<EditForm Model="Opnieuw" FormName="nieuwe-registratiecode" OnValidSubmit="OpnieuwAanvragen" method="post">
|
||||
<div class="kaart">
|
||||
<InputText @bind-Value="Opnieuw.Email" type="hidden" />
|
||||
<InputText @bind-Value="Opnieuw.Voornaam" type="hidden" />
|
||||
<InputText @bind-Value="Opnieuw.Achternaam" type="hidden" />
|
||||
<div class="rij">
|
||||
<div class="groei zacht">Geen e-mail ontvangen? Kijk ook even in je ongewenste post.</div>
|
||||
<button type="submit" class="knop rustig">Nieuwe code aanvragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<p class="zacht">Heb je al een account? <a href="/inloggen">Inloggen</a>.</p>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm(FormName = "registratiecode")] private AanvraagGegevens Aanvraag { get; set; } = new();
|
||||
[SupplyParameterFromForm(FormName = "registreren")] private BevestigGegevens Bevestig { get; set; } = new();
|
||||
[SupplyParameterFromForm(FormName = "nieuwe-registratiecode")] private AanvraagGegevens Opnieuw { get; set; } = new();
|
||||
|
||||
private bool _codeVerstuurd;
|
||||
private string? AfgeschermdAdres;
|
||||
private string? Fout, Gelukt;
|
||||
|
||||
private bool ToonCodeveld =>
|
||||
_codeVerstuurd || !string.IsNullOrWhiteSpace(Bevestig.Email) || !string.IsNullOrWhiteSpace(Opnieuw.Email);
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (AfgeschermdAdres is null && ToonCodeveld)
|
||||
{
|
||||
var adres = !string.IsNullOrWhiteSpace(Bevestig.Email) ? Bevestig.Email : Opnieuw.Email;
|
||||
AfgeschermdAdres = new CodeVerstuurd(adres, DateTime.UtcNow).AfgeschermdAdres;
|
||||
}
|
||||
}
|
||||
|
||||
private Task CodeAanvragen() => StuurCodeAsync(Aanvraag.Email, Aanvraag.Voornaam, Aanvraag.Achternaam);
|
||||
|
||||
private Task OpnieuwAanvragen() => StuurCodeAsync(Opnieuw.Email, Opnieuw.Voornaam, Opnieuw.Achternaam);
|
||||
|
||||
private async Task StuurCodeAsync(string email, string voornaam, string achternaam)
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
var resultaat = await Accounts.VraagRegistratiecodeAanAsync(
|
||||
email, voornaam, achternaam, context.Connection.RemoteIpAddress?.ToString(), context.RequestAborted);
|
||||
|
||||
if (!resultaat.Succeeded) { Fout = resultaat.Error; Gelukt = null; return; }
|
||||
|
||||
_codeVerstuurd = true;
|
||||
AfgeschermdAdres = resultaat.Value!.AfgeschermdAdres;
|
||||
Bevestig = new BevestigGegevens { Email = resultaat.Value.Email };
|
||||
Opnieuw = new AanvraagGegevens { Email = resultaat.Value.Email, Voornaam = voornaam, Achternaam = achternaam };
|
||||
Gelukt = "We hebben je een code gestuurd. Vul hem hieronder in.";
|
||||
Fout = null;
|
||||
}
|
||||
|
||||
private async Task Aanmaken()
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
var resultaat = await Accounts.RegistreerMetCodeAsync(Bevestig.Email, Bevestig.Code, context.RequestAborted);
|
||||
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
_codeVerstuurd = true;
|
||||
AfgeschermdAdres = new CodeVerstuurd(Bevestig.Email, DateTime.UtcNow).AfgeschermdAdres;
|
||||
Fout = resultaat.Error;
|
||||
Gelukt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await Inloggen.MeldAanAsync(context, resultaat.Value!);
|
||||
Navigatie.NavigateTo("/", forceLoad: true);
|
||||
}
|
||||
|
||||
public class AanvraagGegevens
|
||||
{
|
||||
[Required(ErrorMessage = "Vul je voornaam in.")]
|
||||
public string Voornaam { get; set; } = "";
|
||||
|
||||
public string Achternaam { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Je moet een e-mailadres invullen!")]
|
||||
[EmailAddress(ErrorMessage = "Dit is geen geldig e-mailadres!")]
|
||||
public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
public class BevestigGegevens
|
||||
{
|
||||
[Required] public string Email { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Je moet een code invullen!")]
|
||||
public string Code { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@* Uitloggen zit niet meer in een pagina maar in een eigen endpoint; zie Program.cs.
|
||||
|
||||
Waarom: een Blazor-pagina rendert pas als het antwoord al onderweg is. Het Set-Cookie
|
||||
waarmee de aanmelding wordt gewist, kan dan niet meer worden meegestuurd - het uitloggen
|
||||
leek te lukken, maar je bleef gewoon ingelogd.
|
||||
|
||||
Dit bestand is leeg gelaten in plaats van verwijderd, zodat het bij een volgende publicatie
|
||||
niet per ongeluk terugkomt uit een oude map. *@
|
||||
@@ -0,0 +1,88 @@
|
||||
@page "/uitnodiging/{Token}"
|
||||
@inject FamilyService Families
|
||||
@inject AccountService Accounts
|
||||
@inject SamenvoegService Samenvoegen
|
||||
@inject IHttpContextAccessor HttpContexten
|
||||
@inject NavigationManager Navigatie
|
||||
|
||||
<PageTitle>Uitnodiging - MijnBesluit</PageTitle>
|
||||
|
||||
<div class="middenblok">
|
||||
<h1>Je bent uitgenodigd</h1>
|
||||
<p class="zacht">
|
||||
Iemand heeft in MijnBesluit een profiel voor je aangemaakt. Neem je de uitnodiging aan, dan
|
||||
is dat profiel vanaf dat moment van jou: alleen jij kunt het dan nog wijzigen.
|
||||
</p>
|
||||
|
||||
@if (Fout is not null)
|
||||
{
|
||||
<div class="melding fout">
|
||||
@Fout
|
||||
@if (LogEerstIn)
|
||||
{
|
||||
<p style="margin:.8rem 0 0">
|
||||
<a class="knop" href="/inloggen?terug=uitnodiging/@Token">Inloggen</a>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<EditForm Model="Invoer" FormName="uitnodiging" OnValidSubmit="Aannemen" method="post">
|
||||
<div class="kaart">
|
||||
<p class="zacht" style="margin-top:0">
|
||||
Je hoeft geen wachtwoord te bedenken. Bij MijnBesluit log je voortaan in met een code
|
||||
die we je per e-mail sturen — naar hetzelfde adres waarop je deze uitnodiging kreeg.
|
||||
</p>
|
||||
<button type="submit" class="knop vol">Uitnodiging aannemen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Token { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Het model van het formulier. Bewust GEEN [SupplyParameterFromForm]: dit formulier heeft
|
||||
/// geen invoervelden, er valt dus niets te binden.
|
||||
///
|
||||
/// Met dat kenmerk erop ging het juist mis. Het kenmerk stond er zonder FormName, terwijl
|
||||
/// het formulier FormName="uitnodiging" heeft. Die twee horen bij elkaar; kloppen ze niet,
|
||||
/// dan zet de binder de waarde op null en is de "= new()" hierboven weg. EditForm kreeg dan
|
||||
/// een Model van null en gooide "EditForm requires either a Model parameter" - een harde
|
||||
/// fout bij het openen van de uitnodigingslink, nog voordat er iets te accepteren viel.
|
||||
/// </summary>
|
||||
private LeegFormulier Invoer { get; } = new();
|
||||
|
||||
private string? Fout;
|
||||
|
||||
/// <summary>Staat er een knop naar het inlogscherm bij de melding?</summary>
|
||||
private bool LogEerstIn;
|
||||
|
||||
private async Task Aannemen()
|
||||
{
|
||||
var context = HttpContexten.HttpContext!;
|
||||
|
||||
// Ben je al ingelogd, dan neem je deze uitnodiging aan als jezelf en gaat het profiel
|
||||
// op in dat van jou. Dat is hoe twee mensen die allebei een profiel voor je hebben
|
||||
// aangemaakt tot één persoon worden.
|
||||
var ingelogd = context.User.UserId();
|
||||
|
||||
var resultaat = await Families.AcceptInvitationAsync(
|
||||
Token, Accounts, context.RequestAborted, ingelogd, Samenvoegen);
|
||||
|
||||
if (!resultaat.Succeeded)
|
||||
{
|
||||
Fout = resultaat.Error;
|
||||
LogEerstIn = ingelogd is null && Fout is not null && Fout.Contains("Log eerst in");
|
||||
return;
|
||||
}
|
||||
|
||||
// Al ingelogd? Dan is er niets aan je aanmelding veranderd en gaan we gewoon verder.
|
||||
if (ingelogd is null) await Inloggen.MeldAanAsync(context, resultaat.Value!);
|
||||
|
||||
Navigatie.NavigateTo("/", forceLoad: true);
|
||||
}
|
||||
|
||||
/// <summary>De uitnodigingslink is zelf het bewijs; er hoeft niets ingevuld te worden.</summary>
|
||||
public class LeegFormulier { }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@page "/wat-is-mijnbesluit"
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
<PageTitle>Wat is MijnBesluit?</PageTitle>
|
||||
|
||||
<div class="middenblok" style="max-width:640px">
|
||||
<h1>Wat is MijnBesluit?</h1>
|
||||
|
||||
@* De tekst zelf staat in Productuitleg, want de app toont hem ook - daar als popup vanaf
|
||||
het overzicht. Kop en knoppen staan hier omdat ze per plek verschillen. *@
|
||||
<Productuitleg />
|
||||
|
||||
<p style="margin-top:1.5rem">
|
||||
<a class="knop" href="/registreren">Een account aanmaken</a>
|
||||
<a class="knop rustig" href="/inloggen">Inloggen</a>
|
||||
</p>
|
||||
|
||||
<p class="voet"><a href="/inloggen">Terug naar inloggen</a></p>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectNaarInloggen />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,20 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using MijnBesluit.Web
|
||||
@using MijnBesluit.Web.Components
|
||||
@using MijnBesluit.Web.Components.Account
|
||||
@using MijnBesluit.Web.Components.Onderdelen
|
||||
@using MijnBesluit.Application.Models
|
||||
@using MijnBesluit.Application.Services
|
||||
@using MijnBesluit.Domain.Entities
|
||||
@using MijnBesluit.Domain.Enums
|
||||
@using MijnBesluit.Domain.Rules
|
||||
Reference in New Issue
Block a user