Versiehistorie in een tabel, voor site en app

De versiehistorie stond als tekst in zowel de app als de website. Nu staat hij in
tabel version_history, met een beheerpagina (Beheer > Versiehistorie) op de site.
De site leest de tabel rechtstreeks, de app via de nieuwe actie app.versiehistorie.
Startrijen: db/handmatig/22b_versiehistorie_startrijen.sql (als root).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MGLqt1uKfNARQHut9ifcx
This commit is contained in:
jf
2026-10-20 11:47:06 +00:00
co-authored by Claude Sonnet 5.5
parent 90c58fc42e
commit 5446dd4170
28 changed files with 996 additions and 108 deletions
@@ -1,61 +1,89 @@
@* De versiehistorie: nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
@* 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.
De regels staan in een tabel op de server (version_history) en worden beheerd op de website
onder Beheer > Versiehistorie. Site en app lezen dezelfde tabel; dit bestand bevat zelf geen
tekst meer, dus een nieuwe regel vraagt geen nieuwe versie van de app. Zie docs/Versiehistorie.md.
Wat hier NIET in hoort: bugfixes, tekstuele correcties, opmaak die is rechtgezet. Dit is voor
Wat daar 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. *@
@inject VersiehistorieService Historie
<p class="zacht" style="margin-top:0">
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
</p>
<h2 style="margin-top:0">20 oktober 2026 &middot; versie 1.1</h2>
<ul style="padding-left:1.2rem">
<li>
<strong>Meer soorten in de kluis.</strong> Naast wachtwoorden, polissen en uitvaartwensen
kan er voortaan een nieuwe soort bijkomen, met eigen invulvelden, zonder dat je de app
daarvoor opnieuw hoeft te updaten. Je bestaande kluisitems blijven zoals ze waren.
</li>
</ul>
@if (Fout)
{
<p class="zacht">De versiehistorie kon niet worden opgehaald. Probeer het later nog eens.</p>
}
else if (Regels is null)
{
<p class="zacht">Bezig met laden&hellip;</p>
}
else if (Regels.Count == 0)
{
<p class="zacht">Er staat nog niets in de versiehistorie.</p>
}
else
{
@foreach (var groep in Groepen())
{
<h2 style="@(groep.Eerste ? "margin-top:0" : null)">@groep.Kop</h2>
<ul style="padding-left:1.2rem">
@foreach (var regel in groep.Regels)
{
<li style="margin-bottom:.5rem"><strong>@Punt(regel.Kop)</strong> @regel.Tekst</li>
}
</ul>
}
}
<h2>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>
@code {
private IReadOnlyList<VersieRegelWeergave>? Regels;
private bool Fout;
<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>
protected override async Task OnInitializedAsync()
{
try { Regels = await Historie.ZichtbaarAsync(); }
catch { Fout = true; }
}
private sealed record Groep(string Kop, bool Eerste, List<VersieRegelWeergave> Regels);
/// <summary>
/// Opeenvolgende regels met dezelfde dag (en dezelfde versie) vormen samen één kop. De volgorde
/// komt van de server: nieuwste dag bovenaan, binnen een dag in de volgorde van invoer.
/// </summary>
private IEnumerable<Groep> Groepen()
{
var uit = new List<Groep>();
string? vorige = null;
foreach (var r in Regels!)
{
var kop = Koptekst(r);
if (kop != vorige)
{
uit.Add(new Groep(kop, uit.Count == 0, new List<VersieRegelWeergave>()));
vorige = kop;
}
uit[^1].Regels.Add(r);
}
return uit;
}
private static string Koptekst(VersieRegelWeergave r)
{
var dag = DateOnly.TryParseExact(r.Datum, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var d)
? $"{d.Day} {Datums.Maanden[d.Month - 1]} {d.Year}"
: r.Datum;
return string.IsNullOrWhiteSpace(r.Versie) ? dag : $"{dag} \u00b7 versie {r.Versie}";
}
/// <summary>De kop eindigt op een punt, zodat de tekst erachter als gewone zin leest.</summary>
private static string Punt(string kop)
{
var k = kop.Trim();
return k.Length == 0 || ".!?".Contains(k[^1]) ? k : k + ".";
}
}
@@ -35,6 +35,9 @@ else
Ook een eigen scherm: dat is inrichten, geen terugdraaien. *@
<p><a href="/beheer/soorten">Kluissoorten beheren &rsaquo;</a></p>
@* Versiehistorie: de lijst "wat is er nieuw" op de website en in de app. Eén tabel voor beide. *@
<p><a href="/beheer/versiehistorie">Versiehistorie beheren &rsaquo;</a></p>
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
@* ------------------------------------------------------------ statistieken *@
@@ -0,0 +1,256 @@
@page "/beheer/versiehistorie"
@attribute [Authorize]
@inject BeheerService Beheerkant
@inject VersiehistorieService Historie
@rendermode InteractiveServer
<PageTitle>Versiehistorie - Beheer</PageTitle>
@* Het beheerscherm voor de versiehistorie: de lijst "wat is er nieuw" die op de website en in de
app onder "Versiehistorie" staat. Eén tabel voor beide, dus hier een regel toevoegen is genoeg.
Een regel is: een datum, eventueel een appversie, een vet kopje en een tekst. Gewone tekst,
geen opmaak. Wat erin hoort: nieuwe of wezenlijk gewijzigde functies; geen bugfixes.
Een regel verbergen haalt hem weg bij de lezers zonder hem kwijt te raken. Wie de app al
heeft van vóór deze tabel (versiecode 3 of ouder) ziet de tekst die in die app is ingebakken.
Zie docs/Versiehistorie.md. *@
@if (!Magdit)
{
<h1>Niet gevonden</h1>
<p class="zacht">Deze pagina bestaat niet, of hij is niet voor jou.</p>
}
else
{
<h1>Versiehistorie</h1>
<p class="zacht">
Wat er nieuw is, nieuwste bovenaan. Een nieuwe regel staat meteen op de website en in de app,
zonder dat er een nieuwe versie van de app nodig is.
</p>
<p><a href="/beheer">&lsaquo; Terug naar beheer</a></p>
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
@* ------------------------------------------------------------ nieuwe of bestaande regel *@
<div class="kaart">
<h2 style="margin-top:0">@(BewerkId is null ? "Nieuwe regel" : "Regel wijzigen")</h2>
<div class="raster twee">
<div class="veld">
<label for="vh-datum">Datum</label>
<input id="vh-datum" type="date" @bind="Formulier.Datum" />
</div>
<div class="veld">
<label for="vh-versie">Versie (mag leeg)</label>
<input id="vh-versie" type="text" maxlength="20" @bind="Formulier.Versie" @bind:event="oninput"
placeholder="Bijvoorbeeld: 1.2" />
<div class="uitleg">Alleen invullen als dit bij een uitgebrachte versie van de app hoort.</div>
</div>
</div>
<div class="veld">
<label for="vh-kop">Vet kopje</label>
<input id="vh-kop" type="text" maxlength="@VersiehistorieService.MaxKop" @bind="Formulier.Kop" @bind:event="oninput"
placeholder="Bijvoorbeeld: Meer soorten in de kluis." />
</div>
<div class="veld">
<label for="vh-tekst">Tekst</label>
<textarea id="vh-tekst" rows="4" maxlength="@VersiehistorieService.MaxTekst" @bind="Formulier.Tekst" @bind:event="oninput"></textarea>
<div class="uitleg">@Formulier.Tekst.Length van @VersiehistorieService.MaxTekst tekens. Gewone tekst, geen opmaak.</div>
</div>
<div class="keuze">
<input type="checkbox" id="vh-zichtbaar" @bind="Formulier.Zichtbaar" />
<label for="vh-zichtbaar">Zichtbaar voor iedereen</label>
</div>
<div class="rij" style="margin-top:.8rem">
<button class="knop" disabled="@Bezig" @onclick="Opslaan">@(BewerkId is null ? "Regel toevoegen" : "Wijzigingen opslaan")</button>
@if (BewerkId is not null)
{
<button class="knop rustig" disabled="@Bezig" @onclick="Annuleren">Annuleren</button>
}
</div>
</div>
@* ------------------------------------------------------------ de lijst *@
@if (Lijst is null)
{
<p class="zacht">Bezig met laden&hellip;</p>
}
else if (Lijst.Count == 0)
{
<p class="zacht">Er staat nog niets in de versiehistorie.</p>
}
else
{
<div class="stapel">
@foreach (var r in Lijst)
{
<div class="kaart">
<div class="rij">
<div class="groei">
<div class="zacht">
@Datums.Datum(r.Datum)
@if (!string.IsNullOrWhiteSpace(r.Versie)) { <text> &middot; versie @r.Versie</text> }
</div>
<div class="groot">@r.Kop</div>
<div>@r.Tekst</div>
@if (!r.Zichtbaar)
{
<div style="margin-top:.35rem"><span class="label uitgenodigd">Verborgen</span></div>
}
</div>
</div>
<div class="rij" style="margin-top:.6rem">
<button class="knop rustig" disabled="@Bezig" @onclick="() => Wijzigen(r)">Wijzigen</button>
<button class="knop rustig" disabled="@Bezig" @onclick="() => ZichtbaarOmzetten(r)">
@(r.Zichtbaar ? "Verbergen" : "Tonen")
</button>
@if (Weghalen == r.Id)
{
<span class="zacht">Echt verwijderen?</span>
<button class="knop gevaar" disabled="@Bezig" @onclick="() => Verwijderen(r)">Ja, verwijderen</button>
<button class="knop rustig" disabled="@Bezig" @onclick="() => Weghalen = null">Nee</button>
}
else
{
<button class="knop gevaar" disabled="@Bezig" @onclick="() => Weghalen = r.Id">Verwijderen</button>
}
</div>
</div>
}
</div>
}
}
@code {
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
private long _gebruikerId;
private bool Magdit, Bezig;
private string? Melding, MeldingSoort;
private IReadOnlyList<VersieRegelBeheerbeeld>? Lijst;
private int? BewerkId;
private int? Weghalen;
private RegelFormulier Formulier = NieuwFormulier();
private sealed class RegelFormulier
{
public DateOnly Datum { get; set; }
public string Versie { get; set; } = "";
public string Kop { get; set; } = "";
public string Tekst { get; set; } = "";
public bool Zichtbaar { get; set; } = true;
}
private static RegelFormulier NieuwFormulier() =>
new() { Datum = DateOnly.FromDateTime(DateTime.Today) };
protected override async Task OnInitializedAsync()
{
if (AuthState is null) return;
_gebruikerId = (await AuthState).User.UserId() ?? 0;
Magdit = await Beheerkant.IsBeheerderAsync(_gebruikerId);
if (!Magdit) return;
await Laden();
}
private async Task Laden()
{
var resultaat = await Historie.OverzichtAsync(_gebruikerId);
if (resultaat.Succeeded) Lijst = resultaat.Value;
else Meld(resultaat.Error, false);
}
private void Meld(string? tekst, bool goed)
{
Melding = tekst;
MeldingSoort = goed ? "goed" : "fout";
}
private void Wijzigen(VersieRegelBeheerbeeld r)
{
BewerkId = r.Id;
Weghalen = null;
Melding = null;
Formulier = new RegelFormulier
{
Datum = r.Datum, Versie = r.Versie ?? "", Kop = r.Kop, Tekst = r.Tekst, Zichtbaar = r.Zichtbaar
};
}
private void Annuleren()
{
BewerkId = null;
Formulier = NieuwFormulier();
}
private async Task Opslaan()
{
Bezig = true;
try
{
var versie = string.IsNullOrWhiteSpace(Formulier.Versie) ? null : Formulier.Versie.Trim();
bool gelukt;
string? fout;
if (BewerkId is null)
{
var gemaakt = await Historie.MaakAsync(_gebruikerId, Formulier.Datum, versie, Formulier.Kop,
Formulier.Tekst, Formulier.Zichtbaar);
gelukt = gemaakt.Succeeded;
fout = gemaakt.Error;
}
else
{
var gewijzigd = await Historie.WijzigAsync(_gebruikerId, BewerkId.Value, Formulier.Datum, versie,
Formulier.Kop, Formulier.Tekst, Formulier.Zichtbaar);
gelukt = gewijzigd.Succeeded;
fout = gewijzigd.Error;
}
if (gelukt)
{
Meld(BewerkId is null ? "De regel is toegevoegd." : "De wijziging is opgeslagen.", true);
BewerkId = null;
Formulier = NieuwFormulier();
await Laden();
}
else Meld(fout, false);
}
finally { Bezig = false; }
}
private async Task ZichtbaarOmzetten(VersieRegelBeheerbeeld r)
{
Bezig = true;
try
{
var resultaat = await Historie.WijzigAsync(_gebruikerId, r.Id, r.Datum, r.Versie, r.Kop, r.Tekst, !r.Zichtbaar);
if (resultaat.Succeeded)
{
Meld(r.Zichtbaar ? "De regel is verborgen." : "De regel is weer zichtbaar.", true);
await Laden();
}
else Meld(resultaat.Error, false);
}
finally { Bezig = false; }
}
private async Task Verwijderen(VersieRegelBeheerbeeld r)
{
Bezig = true;
try
{
var resultaat = await Historie.VerwijderAsync(_gebruikerId, r.Id);
if (resultaat.Succeeded)
{
Meld("De regel is verwijderd.", true);
Weghalen = null;
if (BewerkId == r.Id) Annuleren();
await Laden();
}
else Meld(resultaat.Error, false);
}
finally { Bezig = false; }
}
}
@@ -178,8 +178,7 @@
</Popup>
}
@* Zelfde tekst als in de app: Versiehistorie.razor staat twee keer op schijf en een
zelfcontrole vergelijkt ze letterlijk. *@
@* De tekst komt uit de tabel version_history, net als in de app. *@
@if (VersiehistorieOpen)
{
<Popup Titel="Versiehistorie" Bevestigtekst="Sluiten" AlleenSluiten="true"