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
+22
View File
@@ -27,6 +27,7 @@ public class Store
public List<CampagneUur> CampagneUren = new();
public List<VaultCategoryDefinition> Soorten = VaultCategorySaatgoed.Maak();
public List<VaultCategoryField> SoortVelden = new();
public List<VersieRegel> VersieRegels = new();
private long _seq;
public long NextId() => ++_seq;
@@ -533,6 +534,27 @@ public class TestPush : IPushSender
/// <summary>De startrijen uit db/21_kluissoorten.sql, zodat de tests met dezelfde soorten beginnen als de echte database.</summary>
public class VersiehistorieRepo : IVersiehistorieRepository
{
private readonly Store _s; public VersiehistorieRepo(Store s) => _s = s;
private int _volgende = 1;
public Task<IReadOnlyList<VersieRegel>> ListAsync(bool alleenZichtbaar, CancellationToken ct = default) =>
Task.FromResult<IReadOnlyList<VersieRegel>>(
_s.VersieRegels.Where(r => !alleenZichtbaar || r.Zichtbaar)
.OrderByDescending(r => r.Datum).ThenBy(r => r.Id).ToList());
public Task<VersieRegel?> FindAsync(int id, CancellationToken ct = default) =>
Task.FromResult(_s.VersieRegels.FirstOrDefault(r => r.Id == id));
// Hoofdletterongevoelig, zoals de database (utf8mb4_unicode_ci) ook doet.
public Task<bool> BestaatAsync(DateOnly datum, string kop, int? behalveId, CancellationToken ct = default) =>
Task.FromResult(_s.VersieRegels.Any(r => r.Datum == datum && string.Equals(r.Kop, kop, StringComparison.OrdinalIgnoreCase) && (behalveId == null || r.Id != behalveId)));
public void Add(VersieRegel regel) { if (regel.Id == 0) regel.Id = _volgende++; _s.VersieRegels.Add(regel); }
public void Remove(VersieRegel regel) => _s.VersieRegels.Remove(regel);
}
public static class VaultCategorySaatgoed
{
public static List<VaultCategoryDefinition> Maak()
+2 -1
View File
@@ -473,7 +473,7 @@ runner.Test("Gedeelde onderdelen zijn in app en website letterlijk gelijk", () =
// Privacytekst: de privacyverklaring, op de site een pagina en in de app een popup. Twee
// versies daarvan is het ergste geval van allemaal - het is het document waar iemand zich op
// beroept, en waarvan Google Play een adres wil.
foreach (var naam in new[] { "Penning.razor", "Productuitleg.razor", "Privacytekst.razor", "Versiehistorie.razor" })
foreach (var naam in new[] { "Penning.razor", "Productuitleg.razor", "Privacytekst.razor" })
{
var web = Path.Combine(map.FullName, "src", "MijnBesluit.Web", "Components", "Onderdelen", naam);
var app = Path.Combine(map.FullName, "src", "MijnBesluit.AppUi", "Onderdelen", naam);
@@ -1943,5 +1943,6 @@ ServiceChecks.Register(runner);
AdvertentieChecks.Register(runner);
UitrolChecks.Register(runner);
SoortenChecks.Register(runner);
VersiehistorieChecks.Register(runner);
return runner.Summary();
+2
View File
@@ -26,6 +26,7 @@ public class World
public AdvertentieService Adverteren { get; }
public AccountOpheffenService Opheffen { get; }
public VaultCategoryService Soorten { get; }
public VersiehistorieService Versiehistorie { get; }
/// <summary>Maakt in één stap een account aan; scheelt in de scenario's het hele codegedoe.</summary>
public async Task<MijnBesluit.Domain.Entities.User> MaakGebruikerAsync(string email, string voornaam, string achternaam = "")
@@ -108,6 +109,7 @@ public class World
Families = new FamilyService(families, people, users, invitations, messages, legacy,
Email, audit, uow, Clock);
Soorten = new VaultCategoryService(new VaultCategoryRepo(Store), users, audit, uow, Clock);
Versiehistorie = new VersiehistorieService(new VersiehistorieRepo(Store), users, audit, uow, Clock);
Vault = new VaultService(vault, people, messages, users, keys, files, audit, uow, Clock, Verkleiner, families, legacy, Soorten);
Meldingen = new MeldingService(new ToestelRepo(Store), users, Push, uow, Clock);
Messages = new MessageService(messages, people, users, vault, keys, files, Email, audit, uow, Clock, Meldingen);
@@ -0,0 +1,139 @@
using MijnBesluit.Application.Models;
using MijnBesluit.Application.Services;
using MijnBesluit.Domain.Entities;
using MijnBesluit.Domain.Enums;
namespace MijnBesluit.SelfCheck;
/// <summary>
/// De versiehistorie uit de database: wie hem mag beheren en wat een gebruiker te zien krijgt.
///
/// De afspraak is eenvoudig: één bron voor site en app. Een gebruiker leest alleen de zichtbare
/// regels, nieuwste dag bovenaan; alleen een beheerder maakt, wijzigt, verbergt of verwijdert.
/// </summary>
public static class VersiehistorieChecks
{
private static async Task<User> BeheerderAsync(World w, string email = "beheer@example.nl")
{
var u = await w.MaakGebruikerAsync(email, "Beheer", "Er");
w.Store.Users.Single(x => x.Id == u.Id).IsBeheerder = true;
return u;
}
private static async Task<int> MaakAsync(World w, User beheerder, DateOnly datum, string kop, string? versie = null,
string tekst = "Uitleg.", bool zichtbaar = true)
{
var r = await w.Versiehistorie.MaakAsync(beheerder.Id, datum, versie, kop, tekst, zichtbaar);
if (!r.Succeeded) throw new AssertionException($"regel maken mislukte: {r.Error}");
return r.Value;
}
public static void Register(Runner runner)
{
runner.TestAsync("Versiehistorie: zonder regels is de lijst leeg", async () =>
{
var w = new World();
Assert.True((await w.Versiehistorie.ZichtbaarAsync()).Count == 0, "een lege tabel geeft een lege lijst");
});
runner.TestAsync("Versiehistorie: alleen een beheerder maakt, wijzigt of verwijdert", async () =>
{
var w = new World();
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
var beheer = await BeheerderAsync(w);
var dag = new DateOnly(2026, 3, 1);
var id = await MaakAsync(w, beheer, dag, "Eerste");
Assert.True(!(await w.Versiehistorie.MaakAsync(jan.Id, dag, null, "Tweede", "Uitleg")).Succeeded,
"een gewone gebruiker maakt geen regel");
Assert.True(!(await w.Versiehistorie.OverzichtAsync(jan.Id)).Succeeded, "en ziet het beheeroverzicht niet");
Assert.True(!(await w.Versiehistorie.WijzigAsync(jan.Id, id, dag, null, "Anders", "Uitleg", true)).Succeeded,
"en wijzigt niets");
Assert.True(!(await w.Versiehistorie.VerwijderAsync(jan.Id, id)).Succeeded, "en verwijdert niets");
Assert.True((await w.Versiehistorie.ZichtbaarAsync()).Single().Kop == "Eerste", "de regel is onaangetast");
});
runner.TestAsync("Versiehistorie: nieuwste dag bovenaan, binnen een dag in invoervolgorde", async () =>
{
var w = new World();
var beheer = await BeheerderAsync(w);
await MaakAsync(w, beheer, new DateOnly(2026, 2, 1), "Oud, eerste");
await MaakAsync(w, beheer, new DateOnly(2026, 4, 1), "Nieuw, eerste", "1.1");
await MaakAsync(w, beheer, new DateOnly(2026, 2, 1), "Oud, tweede");
await MaakAsync(w, beheer, new DateOnly(2026, 4, 1), "Nieuw, tweede", "1.1");
var lijst = await w.Versiehistorie.ZichtbaarAsync();
Assert.True(lijst.Select(r => r.Kop).SequenceEqual(
new[] { "Nieuw, eerste", "Nieuw, tweede", "Oud, eerste", "Oud, tweede" }),
"de nieuwste dag staat bovenaan en binnen een dag de volgorde van invoer");
Assert.True(lijst[0].Datum == "2026-04-01" && lijst[0].Versie == "1.1", "datum als jjjj-mm-dd, met versie");
Assert.True(lijst[2].Versie is null, "een regel zonder versie heeft er geen");
});
runner.TestAsync("Versiehistorie: een verborgen regel ziet alleen de beheerder", async () =>
{
var w = new World();
var beheer = await BeheerderAsync(w);
var dag = new DateOnly(2026, 3, 1);
await MaakAsync(w, beheer, dag, "Zichtbaar");
var verborgen = await MaakAsync(w, beheer, dag, "Nog niet", zichtbaar: false);
Assert.True((await w.Versiehistorie.ZichtbaarAsync()).Select(r => r.Kop).SequenceEqual(new[] { "Zichtbaar" }),
"gebruikers zien de verborgen regel niet");
var overzicht = (await w.Versiehistorie.OverzichtAsync(beheer.Id)).Value!;
Assert.True(overzicht.Count == 2, "de beheerder ziet beide");
var w2 = await w.Versiehistorie.WijzigAsync(beheer.Id, verborgen, dag, null, "Nog niet", "Uitleg.", true);
Assert.True(w2.Succeeded, "tonen lukt");
Assert.True((await w.Versiehistorie.ZichtbaarAsync()).Count == 2, "daarna staat hij er ook voor gebruikers");
});
runner.TestAsync("Versiehistorie: wat niet klopt wordt geweigerd", async () =>
{
var w = new World();
var beheer = await BeheerderAsync(w);
var dag = new DateOnly(2026, 3, 1);
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, dag, null, " ", "Uitleg")).Succeeded, "een lege kop mag niet");
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, dag, null, "Kop", "")).Succeeded, "een lege uitleg mag niet");
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, dag, null,
new string('k', VersiehistorieService.MaxKop + 1), "Uitleg")).Succeeded, "een te lange kop mag niet");
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, dag, null, "Kop",
new string('t', VersiehistorieService.MaxTekst + 1))).Succeeded, "een te lange uitleg mag niet");
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, dag, new string('1', VersiehistorieService.MaxVersie + 1),
"Kop", "Uitleg")).Succeeded, "een te lang versienummer mag niet");
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, new DateOnly(2090, 1, 1), null, "Kop", "Uitleg")).Succeeded,
"een datum ver in de toekomst is een typefout");
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, new DateOnly(1999, 1, 1), null, "Kop", "Uitleg")).Succeeded,
"een datum van lang geleden ook");
await MaakAsync(w, beheer, dag, "Dubbel");
Assert.True(!(await w.Versiehistorie.MaakAsync(beheer.Id, dag, null, "dubbel", "Uitleg")).Succeeded,
"dezelfde kop op dezelfde dag staat er al");
Assert.True((await w.Versiehistorie.MaakAsync(beheer.Id, dag.AddDays(1), null, "Dubbel", "Uitleg")).Succeeded,
"op een andere dag mag dezelfde kop wel");
});
runner.TestAsync("Versiehistorie: wijzigen en verwijderen, en het blijft vastgelegd", async () =>
{
var w = new World();
var beheer = await BeheerderAsync(w);
var dag = new DateOnly(2026, 3, 1);
var id = await MaakAsync(w, beheer, dag, "Eerste", tekst: "Oude uitleg");
Assert.True((await w.Versiehistorie.WijzigAsync(beheer.Id, id, dag, " 1.2 ", " Nieuwe kop ", " Nieuwe uitleg ", true)).Succeeded,
"wijzigen lukt");
var regel = (await w.Versiehistorie.ZichtbaarAsync()).Single();
Assert.True(regel.Kop == "Nieuwe kop" && regel.Tekst == "Nieuwe uitleg" && regel.Versie == "1.2",
"spaties eromheen verdwijnen");
Assert.True(!(await w.Versiehistorie.WijzigAsync(beheer.Id, 9999, dag, null, "Kop", "Uitleg", true)).Succeeded,
"een regel die er niet is, is niet te wijzigen");
Assert.True((await w.Versiehistorie.VerwijderAsync(beheer.Id, id)).Succeeded, "verwijderen lukt");
Assert.True((await w.Versiehistorie.ZichtbaarAsync()).Count == 0, "en dan is hij weg");
var acties = w.Store.Audit.Count(a => a.Action == AuditAction.VersiehistorieBeheerd);
Assert.True(acties == 3, "maken, wijzigen en verwijderen staan alle drie in het logboek");
});
}
}