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:
@@ -55,6 +55,7 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`campagne_uren` TO 'm
|
|||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`sessies` TO 'mijnbesluit_app'@'localhost';
|
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`sessies` TO 'mijnbesluit_app'@'localhost';
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`vault_categories` TO 'mijnbesluit_app'@'localhost';
|
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`vault_categories` TO 'mijnbesluit_app'@'localhost';
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`vault_category_fields` TO 'mijnbesluit_app'@'localhost';
|
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`vault_category_fields` TO 'mijnbesluit_app'@'localhost';
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`version_history` TO 'mijnbesluit_app'@'localhost';
|
||||||
|
|
||||||
-- Het logboek mag er alleen bij komen en uit gelezen worden, niet uit verdwijnen.
|
-- Het logboek mag er alleen bij komen en uit gelezen worden, niet uit verdwijnen.
|
||||||
GRANT SELECT, INSERT ON `mijnbesluit`.`audit_log` TO 'mijnbesluit_app'@'localhost';
|
GRANT SELECT, INSERT ON `mijnbesluit`.`audit_log` TO 'mijnbesluit_app'@'localhost';
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- MijnBesluit - de versiehistorie in de database in plaats van in de code
|
||||||
|
--
|
||||||
|
-- De lijst "wat is er nieuw" stond vast in twee bestanden (Versiehistorie.razor in de site en in de
|
||||||
|
-- app), en moest dus bij elke wijziging op twee plekken worden aangepast. En de app die al in de
|
||||||
|
-- Play Store staat kon een nieuwe regel nooit tonen. Nu staat de lijst hier: de beheerder
|
||||||
|
-- onderhoudt hem op de website (/beheer/versiehistorie), de site leest hem uit de database en de
|
||||||
|
-- app vraagt hem aan de server. Een nieuwe regel is meteen overal te zien.
|
||||||
|
--
|
||||||
|
-- version_history een regel per nieuwe of wezenlijk gewijzigde functie. Regels met dezelfde
|
||||||
|
-- datum (en versie) worden samen onder één kop getoond.
|
||||||
|
--
|
||||||
|
-- Regels die de server afdwingt (niet de database):
|
||||||
|
-- * Alleen een beheerder maakt, wijzigt of verwijdert een regel.
|
||||||
|
-- * Een verborgen regel (zichtbaar = 0) ziet alleen de beheerder.
|
||||||
|
-- * Platte tekst: er gaat geen opmaak of HTML door.
|
||||||
|
--
|
||||||
|
-- Draaien met:
|
||||||
|
-- mysql -u root -p mijnbesluit < db\22_versiehistorie.sql
|
||||||
|
--
|
||||||
|
-- Opnieuw draaien mag: er gebeurt dan niets.
|
||||||
|
--
|
||||||
|
-- Na dit script, eenmalig als root:
|
||||||
|
-- mysql -u root -p < db\02_applicatiegebruiker.sql (rechten op de nieuwe tabel)
|
||||||
|
-- mysql -u root -p mijnbesluit < db\handmatig\22b_versiehistorie_startrijen.sql (de bestaande regels)
|
||||||
|
|
||||||
|
USE `mijnbesluit`;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `version_history` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`datum` DATE NOT NULL, -- de dag waarop de functie er kwam
|
||||||
|
`versie` VARCHAR(20) NULL, -- bijvoorbeeld 1.1; leeg als het geen app-versie betreft
|
||||||
|
`kop` VARCHAR(120) NOT NULL, -- het vette stukje: wat kun je nu
|
||||||
|
`tekst` VARCHAR(1000) NOT NULL, -- de uitleg erachter, platte tekst
|
||||||
|
`zichtbaar` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`aangemaakt_op` DATETIME(3) NOT NULL,
|
||||||
|
`gewijzigd_op` DATETIME(3) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `ux_version_history_datum_kop` (`datum`, `kop`),
|
||||||
|
KEY `ix_version_history_datum` (`datum`)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- Startrijen staan NIET hier maar in handmatig\22b_versiehistorie_startrijen.sql: de uitrolagent
|
||||||
|
-- draait dit script met een account dat geen gegevens mag invoegen (zie 21_kluissoorten.sql).
|
||||||
|
|
||||||
|
SELECT 'Klaar. De tabel voor de versiehistorie staat klaar. Vergeet handmatig\\22b_versiehistorie_startrijen.sql en 02_applicatiegebruiker.sql niet (als root).' AS melding;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-- MijnBesluit - de bestaande versiehistorie als startrijen
|
||||||
|
--
|
||||||
|
-- Dit zijn de regels die tot nu toe in Versiehistorie.razor stonden, plus die van versie 1.1.
|
||||||
|
-- Eenmalig te draaien als root, ná db\22_versiehistorie.sql en db\02_applicatiegebruiker.sql:
|
||||||
|
-- mysql -u root -p mijnbesluit < db\handmatig\22b_versiehistorie_startrijen.sql
|
||||||
|
--
|
||||||
|
-- Opnieuw draaien mag: INSERT IGNORE slaat regels over die er al staan (zelfde datum en kop).
|
||||||
|
-- Daarna onderhoud je de lijst op de website: Beheer > Versiehistorie.
|
||||||
|
|
||||||
|
USE `mijnbesluit`;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `version_history` (`datum`, `versie`, `kop`, `tekst`, `zichtbaar`, `aangemaakt_op`, `gewijzigd_op`) VALUES
|
||||||
|
('2026-09-19', NULL, 'Met mij gedeeld, ook in de app.',
|
||||||
|
'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.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||||
|
('2026-09-19', NULL, 'Een gedeeld of nagelaten kluisitem bekijken.',
|
||||||
|
'Dat opent nu als pop-up in dezelfde opmaak als het invulformulier, inclusief de foto''s en documenten die erbij horen.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||||
|
('2026-09-19', NULL, 'Met de vinger naar beneden trekken ververst het scherm.',
|
||||||
|
'Werkt op Kluis, Mijn mensen, Momenten en Nalatenschap.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||||
|
('2026-09-20', NULL, 'Rijbewijs en paspoort in de kluis.',
|
||||||
|
'Twee nieuwe categorieën, met een knop om meteen de voor- en achterkant (rijbewijs) of de foto-pagina (paspoort) te maken of te kiezen.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||||
|
('2026-09-20', NULL, 'Mijn profiel als pop-up.',
|
||||||
|
'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.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||||
|
('2026-09-20', NULL, 'Duidelijker voor de nalatenschapsbeheerder.',
|
||||||
|
'Bij "Aan mij toevertrouwd" zie je nu meteen hoeveel meldingen er nodig zijn en hoe lang de bezwaartermijn is.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||||
|
('2026-09-20', NULL, 'Muziek en verdere wensen groeien mee.',
|
||||||
|
'Die velden bij een kluisitem passen zich nu vanzelf aan de lengte van de tekst aan, ook op de website.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||||
|
('2026-10-20', '1.1', 'Meer soorten in de kluis.',
|
||||||
|
'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.',
|
||||||
|
1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3));
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS aantal_regels FROM `version_history`;
|
||||||
@@ -76,6 +76,7 @@ aanvoelt. Bij `"Error"` staat in `message` een tekst die je zo aan de gebruiker
|
|||||||
| `vault.save` | ja | Item versleuteld opslaan |
|
| `vault.save` | ja | Item versleuteld opslaan |
|
||||||
| `vault.categories` | ja | De soorten kluisitems die jij te zien krijgt: de actieve plus die waar je al een item van hebt, met naam, uitleg, "bespreken met" en bij een eigen soort de velden van het formulier |
|
| `vault.categories` | ja | De soorten kluisitems die jij te zien krijgt: de actieve plus die waar je al een item van hebt, met naam, uitleg, "bespreken met" en bij een eigen soort de velden van het formulier |
|
||||||
| `vault.category` | ja | Eén soort op nummer, ook als hij niet actief is (nodig om het item van een ander te lezen); alleen de beschrijving, nooit inhoud |
|
| `vault.category` | ja | Eén soort op nummer, ook als hij niet actief is (nodig om het item van een ander te lezen); alleen de beschrijving, nooit inhoud |
|
||||||
|
| `app.versiehistorie` | ja | De zichtbare regels van de versiehistorie (datum jjjj-mm-dd, versie, kop, tekst), nieuwste dag bovenaan; staat in tabel `version_history`, zie docs/Versiehistorie.md |
|
||||||
| `message.inbox` / `message.outbox` | ja | Berichten voor en door jou |
|
| `message.inbox` / `message.outbox` | ja | Berichten voor en door jou |
|
||||||
| `message.read` | ja | Bericht lezen en als gelezen markeren |
|
| `message.read` | ja | Bericht lezen en als gelezen markeren |
|
||||||
| `message.compose` | ja | Bericht klaarzetten |
|
| `message.compose` | ja | Bericht klaarzetten |
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Versiehistorie
|
||||||
|
|
||||||
|
De lijst "wat is er nieuw" die onder **Versiehistorie** staat, op de website (pop-up vanaf het
|
||||||
|
overzicht) en in de app (onderaan het startscherm), staat in de database. Een nieuwe regel vraagt
|
||||||
|
daarom **geen nieuwe versie van de app** in de Play Store of App Store, en je houdt het op één
|
||||||
|
plek bij.
|
||||||
|
|
||||||
|
## Hoe het werkt
|
||||||
|
|
||||||
|
* Tabel `version_history`: één rij per regel, met `datum`, `versie` (mag leeg), `kop` (het vette
|
||||||
|
kopje), `tekst`, `zichtbaar`. Gewone tekst, geen opmaak. Dezelfde kop op dezelfde dag kan maar
|
||||||
|
één keer voorkomen.
|
||||||
|
* De website leest de tabel rechtstreeks (`VersiehistorieService.ZichtbaarAsync`). De app vraagt
|
||||||
|
hem op bij de server met de actie `app.versiehistorie` (zie `Socketprotocol.md`) en toont een
|
||||||
|
melding als dat niet lukt.
|
||||||
|
* Volgorde: nieuwste dag bovenaan, binnen één dag in de volgorde van invoer. Opeenvolgende regels
|
||||||
|
met dezelfde dag en versie komen onder één kop te staan ("20 oktober 2026 · versie 1.1").
|
||||||
|
* Beheren doet een beheerder op de website: **Beheer > Versiehistorie beheren**. Toevoegen,
|
||||||
|
wijzigen, verbergen en verwijderen; elke wijziging komt in het auditlogboek
|
||||||
|
(`VersiehistorieBeheerd`). Een verborgen regel zien lezers niet meer, maar is niet kwijt.
|
||||||
|
|
||||||
|
## Wat erin hoort
|
||||||
|
|
||||||
|
Nieuwe of wezenlijk gewijzigde functies: "wat kan ik nu wat ik eerst niet kon". Geen bugfixes,
|
||||||
|
geen tekstuele correcties, geen opmaak die is rechtgezet.
|
||||||
|
|
||||||
|
## Uitrollen
|
||||||
|
|
||||||
|
1. `db\22_versiehistorie.sql` via de databasescripts van SideKick (maakt de tabel; verandert niets
|
||||||
|
aan bestaande gegevens en is opnieuw te draaien).
|
||||||
|
2. Als root: `mysql -u root -p < db\02_applicatiegebruiker.sql` (rechten voor `mijnbesluit_app`
|
||||||
|
op de nieuwe tabel) en `mysql -u root -p mijnbesluit < db\handmatig\22b_versiehistorie_startrijen.sql`
|
||||||
|
(de bestaande regels; `INSERT IGNORE`, dus opnieuw draaien is veilig).
|
||||||
|
3. Daarna pas de website en de SocketServer uitrollen. Andersom leest de nieuwe code een tabel
|
||||||
|
die er nog niet is, of waar de applicatiegebruiker nog niet bij mag.
|
||||||
|
|
||||||
|
## Oudere apps
|
||||||
|
|
||||||
|
De tekst van de versiehistorie zat tot en met appversie 1.1 (versiecode 3) in de app zelf. Die
|
||||||
|
apps lezen de tabel nooit en tonen dus hun ingebakken tekst, ook als je hier iets bijzet. Vanaf
|
||||||
|
versiecode 4 haalt de app de lijst op bij de server.
|
||||||
@@ -1,61 +1,90 @@
|
|||||||
@* 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
|
De regels staan in een tabel op de server (version_history) en worden beheerd op de website
|
||||||
het overzicht, in de app hetzelfde. Dit bestand staat twee keer op schijf - de app en de site
|
onder Beheer > Versiehistorie. Site en app lezen dezelfde tabel; dit bestand bevat zelf geen
|
||||||
delen geen project - en een zelfcontrole vergelijkt ze letterlijk.
|
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. *@
|
"wat kan ik nu wat ik eerst niet kon", niet een logboek van elke wijziging in de code. *@
|
||||||
|
@inject AppToestand Toestand
|
||||||
|
|
||||||
<p class="zacht" style="margin-top:0">
|
<p class="zacht" style="margin-top:0">
|
||||||
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 style="margin-top:0">20 oktober 2026 · versie 1.1</h2>
|
@if (Fout)
|
||||||
<ul style="padding-left:1.2rem">
|
{
|
||||||
<li>
|
<p class="zacht">De versiehistorie kon niet worden opgehaald. Probeer het later nog eens.</p>
|
||||||
<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
|
else if (Regels is null)
|
||||||
daarvoor opnieuw hoeft te updaten. Je bestaande kluisitems blijven zoals ze waren.
|
{
|
||||||
</li>
|
<p class="zacht">Bezig met laden…</p>
|
||||||
</ul>
|
}
|
||||||
|
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>
|
@code {
|
||||||
<ul style="padding-left:1.2rem">
|
private IReadOnlyList<VersieRegel>? Regels;
|
||||||
<li style="margin-bottom:.5rem">
|
private bool Fout;
|
||||||
<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>
|
protected override async Task OnInitializedAsync()
|
||||||
<ul style="padding-left:1.2rem">
|
{
|
||||||
<li style="margin-bottom:.5rem">
|
var uitkomst = await Toestand.Client.VersiehistorieAsync();
|
||||||
<strong>Met mij gedeeld, ook in de app.</strong> Heeft iemand een kluisitem rechtstreeks
|
if (uitkomst.Gelukt) Regels = uitkomst.Waarde;
|
||||||
met jou gedeeld om het vast te bespreken - bijvoorbeeld een uitvaartwens - dan zie je
|
else Fout = true;
|
||||||
dat voortaan ook op je telefoon, niet meer alleen op de website.
|
}
|
||||||
</li>
|
|
||||||
<li style="margin-bottom:.5rem">
|
private sealed record Groep(string Kop, bool Eerste, List<VersieRegel> Regels);
|
||||||
<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
|
/// <summary>
|
||||||
horen.
|
/// Opeenvolgende regels met dezelfde dag (en dezelfde versie) vormen samen één kop. De volgorde
|
||||||
</li>
|
/// komt van de server: nieuwste dag bovenaan, binnen een dag in de volgorde van invoer.
|
||||||
<li>
|
/// </summary>
|
||||||
<strong>Met de vinger naar beneden trekken ververst het scherm.</strong> Werkt op Kluis,
|
private IEnumerable<Groep> Groepen()
|
||||||
Mijn mensen, Momenten en Nalatenschap.
|
{
|
||||||
</li>
|
var uit = new List<Groep>();
|
||||||
</ul>
|
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<VersieRegel>()));
|
||||||
|
vorige = kop;
|
||||||
|
}
|
||||||
|
uit[^1].Regels.Add(r);
|
||||||
|
}
|
||||||
|
return uit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Koptekst(VersieRegel 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 + ".";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,8 +174,7 @@
|
|||||||
</Popup>
|
</Popup>
|
||||||
}
|
}
|
||||||
|
|
||||||
@* Zelfde tekst als op de website - Versiehistorie.razor staat twee keer op schijf en een
|
@* De tekst komt van de server (tabel version_history), net als op de website. *@
|
||||||
zelfcontrole vergelijkt ze letterlijk. *@
|
|
||||||
@if (VersiehistorieOpen)
|
@if (VersiehistorieOpen)
|
||||||
{
|
{
|
||||||
<Popup Titel="Versiehistorie" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
<Popup Titel="Versiehistorie" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||||
|
|||||||
@@ -287,6 +287,21 @@ public interface IVaultCategoryRepository
|
|||||||
void Remove(VaultCategoryField veld);
|
void Remove(VaultCategoryField veld);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>De versiehistorie: één tabel voor site en app. Zie docs/Versiehistorie.md.</summary>
|
||||||
|
public interface IVersiehistorieRepository
|
||||||
|
{
|
||||||
|
/// <summary>Nieuwste dag bovenaan; binnen één dag in de volgorde van invoer.</summary>
|
||||||
|
Task<IReadOnlyList<VersieRegel>> ListAsync(bool alleenZichtbaar, CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<VersieRegel?> FindAsync(int id, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>Is er al een regel met dezelfde datum en kop (behalve de regel met dit nummer)?</summary>
|
||||||
|
Task<bool> BestaatAsync(DateOnly datum, string kop, int? behalveId, CancellationToken ct = default);
|
||||||
|
|
||||||
|
void Add(VersieRegel regel);
|
||||||
|
void Remove(VersieRegel regel);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Versturen van uitnodigingen en meldingen.</summary>
|
/// <summary>Versturen van uitnodigingen en meldingen.</summary>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adverteerders, hun campagnes en de tellers per uur. Zie docs/Adverteren.md.
|
/// Adverteerders, hun campagnes en de tellers per uur. Zie docs/Adverteren.md.
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace MijnBesluit.Application.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Een regel van de versiehistorie zoals een gebruiker die ziet. Dit is ook wat er over de lijn
|
||||||
|
/// naar de app gaat; de datum is als tekst (jjjj-mm-dd) om tijdzones buiten de deur te houden:
|
||||||
|
/// 20 oktober is 20 oktober, waar je ook bent.
|
||||||
|
/// </summary>
|
||||||
|
public record VersieRegelWeergave(string Datum, string? Versie, string Kop, string Tekst);
|
||||||
|
|
||||||
|
/// <summary>Een regel in het beheerscherm, ook als hij verborgen is.</summary>
|
||||||
|
public record VersieRegelBeheerbeeld(int Id, DateOnly Datum, string? Versie, string Kop, string Tekst, bool Zichtbaar);
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using MijnBesluit.Application.Abstractions;
|
||||||
|
using MijnBesluit.Application.Models;
|
||||||
|
using MijnBesluit.Domain.Entities;
|
||||||
|
using MijnBesluit.Domain.Enums;
|
||||||
|
|
||||||
|
namespace MijnBesluit.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// De versiehistorie: wat er nieuw is in MijnBesluit, voor wie dat wil lezen. De lijst staat in de
|
||||||
|
/// database en wordt door de beheerder onderhouden, zodat de website en de app dezelfde bron
|
||||||
|
/// hebben en een nieuwe regel meteen overal staat - ook in een app die al in de winkel staat.
|
||||||
|
///
|
||||||
|
/// Twee regels:
|
||||||
|
///
|
||||||
|
/// 1. Alleen een beheerder maakt, wijzigt of verwijdert. Een gebruiker kan alleen lezen, en ziet
|
||||||
|
/// dan alleen de regels die zichtbaar zijn.
|
||||||
|
/// 2. Platte tekst. Wat hier staat wordt door de schermen als tekst getoond, nooit als opmaak;
|
||||||
|
/// zo kan een typefout of een geplakte tekst het scherm niet stukmaken.
|
||||||
|
/// </summary>
|
||||||
|
public class VersiehistorieService
|
||||||
|
{
|
||||||
|
public const int MaxKop = 120;
|
||||||
|
public const int MaxTekst = 1000;
|
||||||
|
public const int MaxVersie = 20;
|
||||||
|
|
||||||
|
private readonly IVersiehistorieRepository _regels;
|
||||||
|
private readonly IUserRepository _users;
|
||||||
|
private readonly IAuditLog _audit;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
private readonly IClock _clock;
|
||||||
|
|
||||||
|
public VersiehistorieService(IVersiehistorieRepository regels, IUserRepository users, IAuditLog audit,
|
||||||
|
IUnitOfWork uow, IClock clock)
|
||||||
|
{
|
||||||
|
_regels = regels; _users = users; _audit = audit; _uow = uow; _clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ voor gebruikers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// De zichtbare regels, de nieuwste dag bovenaan. Binnen een dag staan ze in de volgorde waarin
|
||||||
|
/// ze zijn ingevoerd.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<VersieRegelWeergave>> ZichtbaarAsync(CancellationToken ct = default) =>
|
||||||
|
(await _regels.ListAsync(alleenZichtbaar: true, ct))
|
||||||
|
.Select(r => new VersieRegelWeergave(Dag(r.Datum), r.Versie, r.Kop, r.Tekst))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ voor de beheerder
|
||||||
|
|
||||||
|
/// <summary>Alle regels, ook de verborgen. Alleen voor beheerders.</summary>
|
||||||
|
public async Task<Result<IReadOnlyList<VersieRegelBeheerbeeld>>> OverzichtAsync(long currentUserId,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!await IsBeheerderAsync(currentUserId, ct))
|
||||||
|
return Result<IReadOnlyList<VersieRegelBeheerbeeld>>.Fail("Je bent geen beheerder.");
|
||||||
|
|
||||||
|
var lijst = (await _regels.ListAsync(alleenZichtbaar: false, ct))
|
||||||
|
.Select(r => new VersieRegelBeheerbeeld(r.Id, r.Datum, r.Versie, r.Kop, r.Tekst, r.Zichtbaar))
|
||||||
|
.ToList();
|
||||||
|
return Result<IReadOnlyList<VersieRegelBeheerbeeld>>.Ok(lijst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Voegt een regel toe. Hij is meteen zichtbaar tenzij je hem verbergt.</summary>
|
||||||
|
public async Task<Result<int>> MaakAsync(long currentUserId, DateOnly datum, string? versie, string kop,
|
||||||
|
string tekst, bool zichtbaar = true, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!await IsBeheerderAsync(currentUserId, ct)) return Result<int>.Fail("Je bent geen beheerder.");
|
||||||
|
|
||||||
|
var fout = await ControleerAsync(null, datum, versie, kop, tekst, ct);
|
||||||
|
if (fout is not null) return Result<int>.Fail(fout);
|
||||||
|
|
||||||
|
var nu = _clock.UtcNow;
|
||||||
|
var regel = new VersieRegel
|
||||||
|
{
|
||||||
|
Datum = datum, Versie = SchoneVersie(versie), Kop = kop.Trim(), Tekst = tekst.Trim(),
|
||||||
|
Zichtbaar = zichtbaar, AangemaaktOp = nu, GewijzigdOp = nu
|
||||||
|
};
|
||||||
|
|
||||||
|
_regels.Add(regel);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
await _audit.WriteAsync(AuditAction.VersiehistorieBeheerd, currentUserId, nameof(VersieRegel), regel.Id,
|
||||||
|
$"gemaakt: {Dag(datum)} {regel.Kop}", ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
return Result<int>.Ok(regel.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Wijzigt een regel, en kan hem verbergen of weer tonen.</summary>
|
||||||
|
public async Task<Result> WijzigAsync(long currentUserId, int id, DateOnly datum, string? versie, string kop,
|
||||||
|
string tekst, bool zichtbaar, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||||
|
|
||||||
|
var regel = await _regels.FindAsync(id, ct);
|
||||||
|
if (regel is null) return Result.Fail("Deze regel bestaat niet.");
|
||||||
|
|
||||||
|
var fout = await ControleerAsync(id, datum, versie, kop, tekst, ct);
|
||||||
|
if (fout is not null) return Result.Fail(fout);
|
||||||
|
|
||||||
|
regel.Datum = datum;
|
||||||
|
regel.Versie = SchoneVersie(versie);
|
||||||
|
regel.Kop = kop.Trim();
|
||||||
|
regel.Tekst = tekst.Trim();
|
||||||
|
regel.Zichtbaar = zichtbaar;
|
||||||
|
regel.GewijzigdOp = _clock.UtcNow;
|
||||||
|
|
||||||
|
await _audit.WriteAsync(AuditAction.VersiehistorieBeheerd, currentUserId, nameof(VersieRegel), id,
|
||||||
|
$"gewijzigd: {Dag(datum)} {regel.Kop}", ct);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
return Result.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Haalt een regel weg. Wil je hem alleen even niet tonen, verberg hem dan.</summary>
|
||||||
|
public async Task<Result> VerwijderAsync(long currentUserId, int id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||||
|
|
||||||
|
var regel = await _regels.FindAsync(id, ct);
|
||||||
|
if (regel is null) return Result.Fail("Deze regel bestaat niet.");
|
||||||
|
|
||||||
|
await _audit.WriteAsync(AuditAction.VersiehistorieBeheerd, currentUserId, nameof(VersieRegel), id,
|
||||||
|
$"verwijderd: {Dag(regel.Datum)} {regel.Kop}", ct);
|
||||||
|
_regels.Remove(regel);
|
||||||
|
await _uow.SaveChangesAsync(ct);
|
||||||
|
return Result.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ hulp
|
||||||
|
|
||||||
|
private static string Dag(DateOnly datum) => datum.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
private static string? SchoneVersie(string? versie) =>
|
||||||
|
string.IsNullOrWhiteSpace(versie) ? null : versie.Trim();
|
||||||
|
|
||||||
|
private async Task<bool> IsBeheerderAsync(long userId, CancellationToken ct) =>
|
||||||
|
(await _users.FindByIdAsync(userId, ct))?.IsBeheerder == true;
|
||||||
|
|
||||||
|
private async Task<string?> ControleerAsync(int? eigenId, DateOnly datum, string? versie, string? kop,
|
||||||
|
string? tekst, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(kop)) return "Geef de regel een kop.";
|
||||||
|
if (kop.Trim().Length > MaxKop) return $"De kop is te lang (hoogstens {MaxKop} tekens).";
|
||||||
|
if (string.IsNullOrWhiteSpace(tekst)) return "Schrijf er een uitleg bij.";
|
||||||
|
if (tekst.Trim().Length > MaxTekst) return $"De uitleg is te lang (hoogstens {MaxTekst} tekens).";
|
||||||
|
if (versie is not null && versie.Trim().Length > MaxVersie) return $"Het versienummer is te lang (hoogstens {MaxVersie} tekens).";
|
||||||
|
|
||||||
|
// Een jaar vooruit mag, voor wie een regel klaarzet; verder is het een typefout.
|
||||||
|
var vandaag = DateOnly.FromDateTime(_clock.UtcNow);
|
||||||
|
if (datum.Year < 2020 || datum > vandaag.AddYears(1)) return "Deze datum klopt niet.";
|
||||||
|
|
||||||
|
if (await _regels.BestaatAsync(datum, kop.Trim(), eigenId, ct))
|
||||||
|
return "Op deze dag staat al een regel met precies deze kop.";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -348,6 +348,13 @@ public class MijnBesluitClient : IAsyncDisposable
|
|||||||
public Task<Uitkomst<KluisSoort>> KluisSoortAsync(int soortId, CancellationToken ct = default) =>
|
public Task<Uitkomst<KluisSoort>> KluisSoortAsync(int soortId, CancellationToken ct = default) =>
|
||||||
StuurAsync<KluisSoort>(SocketActions.VaultCategory, new { id = soortId }, ct);
|
StuurAsync<KluisSoort>(SocketActions.VaultCategory, new { id = soortId }, ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// De versiehistorie zoals de server die bijhoudt, nieuwste dag bovenaan. Een oudere server
|
||||||
|
/// kent de actie niet; het scherm toont dan een melding in plaats van een lijst.
|
||||||
|
/// </summary>
|
||||||
|
public Task<Uitkomst<IReadOnlyList<VersieRegel>>> VersiehistorieAsync(CancellationToken ct = default) =>
|
||||||
|
StuurAsync<IReadOnlyList<VersieRegel>>(SocketActions.Versiehistorie, null, ct);
|
||||||
|
|
||||||
public Task<Uitkomst<KluisInhoud>> KluisOpenenAsync(long itemId, CancellationToken ct = default) =>
|
public Task<Uitkomst<KluisInhoud>> KluisOpenenAsync(long itemId, CancellationToken ct = default) =>
|
||||||
StuurAsync<KluisInhoud>(SocketActions.VaultOpen, new { id = itemId }, ct);
|
StuurAsync<KluisInhoud>(SocketActions.VaultOpen, new { id = itemId }, ct);
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,12 @@ public record KluisVeld(string Sleutel, string Label, string Soort, IReadOnlyLis
|
|||||||
public const string Uitleg = "uitleg";
|
public const string Uitleg = "uitleg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eén regel van de versiehistorie. <c>Datum</c> staat als jjjj-mm-dd; <c>Versie</c> is leeg als
|
||||||
|
/// de regel niet bij een uitgebrachte appversie hoort.
|
||||||
|
/// </summary>
|
||||||
|
public record VersieRegel(string Datum, string? Versie, string Kop, string Tekst);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// De ingebouwde soorten kluisitems, voor als de server er geen lijst van geeft: een oudere
|
/// De ingebouwde soorten kluisitems, voor als de server er geen lijst van geeft: een oudere
|
||||||
/// server, of geen verbinding op het moment dat het scherm opent. Dezelfde nummers, namen en
|
/// server, of geen verbinding op het moment dat het scherm opent. Dezelfde nummers, namen en
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace MijnBesluit.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eén regel in de versiehistorie: een nieuwe of wezenlijk gewijzigde functie, zoals een gebruiker
|
||||||
|
/// die zou willen lezen ("wat kan ik nu wat ik eerst niet kon"). Geen bugfixes, geen opmaak die
|
||||||
|
/// rechtgezet is.
|
||||||
|
///
|
||||||
|
/// De lijst staat in de database en wordt door de beheerder onderhouden; de website leest hem
|
||||||
|
/// rechtstreeks en de app vraagt hem aan de server. Regels met dezelfde datum en versie worden
|
||||||
|
/// samen onder één kop getoond.
|
||||||
|
/// </summary>
|
||||||
|
public class VersieRegel
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>De dag waarop de functie er kwam.</summary>
|
||||||
|
public DateOnly Datum { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Het versienummer van de app, bijvoorbeeld "1.1". Leeg als het alleen de website betreft.</summary>
|
||||||
|
public string? Versie { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Het vette stukje: wat kun je nu.</summary>
|
||||||
|
public string Kop { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>De uitleg erachter. Platte tekst.</summary>
|
||||||
|
public string Tekst { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Een verborgen regel ziet alleen de beheerder; handig voor iets dat nog niet live is.</summary>
|
||||||
|
public bool Zichtbaar { get; set; } = true;
|
||||||
|
|
||||||
|
public DateTime AangemaaktOp { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime GewijzigdOp { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -74,7 +74,8 @@ public enum AuditAction
|
|||||||
NalatenschapToegangVerleend = 60,
|
NalatenschapToegangVerleend = 60,
|
||||||
AdvertentieIngediend = 70, AdvertentieBetaald = 71, AdvertentieGoedgekeurd = 72,
|
AdvertentieIngediend = 70, AdvertentieBetaald = 71, AdvertentieGoedgekeurd = 72,
|
||||||
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74,
|
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74,
|
||||||
KluisSoortBeheerd = 80
|
KluisSoortBeheerd = 80,
|
||||||
|
VersiehistorieBeheerd = 81
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IInvitationRepository, InvitationRepository>();
|
services.AddScoped<IInvitationRepository, InvitationRepository>();
|
||||||
services.AddScoped<IVaultRepository, VaultRepository>();
|
services.AddScoped<IVaultRepository, VaultRepository>();
|
||||||
services.AddScoped<IVaultCategoryRepository, VaultCategoryRepository>();
|
services.AddScoped<IVaultCategoryRepository, VaultCategoryRepository>();
|
||||||
|
services.AddScoped<IVersiehistorieRepository, VersiehistorieRepository>();
|
||||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||||
services.AddScoped<ILegacyRepository, LegacyRepository>();
|
services.AddScoped<ILegacyRepository, LegacyRepository>();
|
||||||
services.AddScoped<IToestelRepository, ToestelRepository>();
|
services.AddScoped<IToestelRepository, ToestelRepository>();
|
||||||
@@ -105,6 +106,7 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
services.AddScoped<AccountService>();
|
services.AddScoped<AccountService>();
|
||||||
services.AddScoped<VaultCategoryService>();
|
services.AddScoped<VaultCategoryService>();
|
||||||
|
services.AddScoped<VersiehistorieService>();
|
||||||
services.AddScoped<VaultService>();
|
services.AddScoped<VaultService>();
|
||||||
// De link naar de landingspagina voor iemand zonder account moet naar de echte site wijzen.
|
// De link naar de landingspagina voor iemand zonder account moet naar de echte site wijzen.
|
||||||
services.AddScoped(sp =>
|
services.AddScoped(sp =>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class MijnBesluitDbContext : DbContext
|
|||||||
public DbSet<VaultFile> VaultFiles => Set<VaultFile>();
|
public DbSet<VaultFile> VaultFiles => Set<VaultFile>();
|
||||||
public DbSet<VaultCategoryDefinition> VaultCategories => Set<VaultCategoryDefinition>();
|
public DbSet<VaultCategoryDefinition> VaultCategories => Set<VaultCategoryDefinition>();
|
||||||
public DbSet<VaultCategoryField> VaultCategoryFields => Set<VaultCategoryField>();
|
public DbSet<VaultCategoryField> VaultCategoryFields => Set<VaultCategoryField>();
|
||||||
|
public DbSet<VersieRegel> VersieRegels => Set<VersieRegel>();
|
||||||
public DbSet<Toestel> Toestellen => Set<Toestel>();
|
public DbSet<Toestel> Toestellen => Set<Toestel>();
|
||||||
public DbSet<Sessie> Sessies => Set<Sessie>();
|
public DbSet<Sessie> Sessies => Set<Sessie>();
|
||||||
public DbSet<ContentGrant> ContentGrants => Set<ContentGrant>();
|
public DbSet<ContentGrant> ContentGrants => Set<ContentGrant>();
|
||||||
@@ -220,6 +221,19 @@ public class MijnBesluitDbContext : DbContext
|
|||||||
e.HasOne(x => x.VaultFile).WithMany().HasForeignKey(x => x.VaultFileId);
|
e.HasOne(x => x.VaultFile).WithMany().HasForeignKey(x => x.VaultFileId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
b.Entity<VersieRegel>(e =>
|
||||||
|
{
|
||||||
|
e.ToTable("version_history");
|
||||||
|
e.Property(x => x.Id).HasColumnName("id");
|
||||||
|
e.Property(x => x.Datum).HasColumnName("datum");
|
||||||
|
e.Property(x => x.Versie).HasColumnName("versie").HasMaxLength(20);
|
||||||
|
e.Property(x => x.Kop).HasColumnName("kop").HasMaxLength(120).IsRequired();
|
||||||
|
e.Property(x => x.Tekst).HasColumnName("tekst").HasMaxLength(1000).IsRequired();
|
||||||
|
e.Property(x => x.Zichtbaar).HasColumnName("zichtbaar");
|
||||||
|
e.Property(x => x.AangemaaktOp).HasColumnName("aangemaakt_op");
|
||||||
|
e.Property(x => x.GewijzigdOp).HasColumnName("gewijzigd_op");
|
||||||
|
});
|
||||||
|
|
||||||
b.Entity<VaultCategoryDefinition>(e =>
|
b.Entity<VaultCategoryDefinition>(e =>
|
||||||
{
|
{
|
||||||
e.ToTable("vault_categories");
|
e.ToTable("vault_categories");
|
||||||
|
|||||||
@@ -267,6 +267,26 @@ public class VaultCategoryRepository : IVaultCategoryRepository
|
|||||||
public void Remove(VaultCategoryField veld) => _db.VaultCategoryFields.Remove(veld);
|
public void Remove(VaultCategoryField veld) => _db.VaultCategoryFields.Remove(veld);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>De versiehistorie; zie <see cref="IVersiehistorieRepository"/>.</summary>
|
||||||
|
public class VersiehistorieRepository : IVersiehistorieRepository
|
||||||
|
{
|
||||||
|
private readonly MijnBesluitDbContext _db;
|
||||||
|
public VersiehistorieRepository(MijnBesluitDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<VersieRegel>> ListAsync(bool alleenZichtbaar, CancellationToken ct = default) =>
|
||||||
|
await _db.VersieRegels.Where(r => !alleenZichtbaar || r.Zichtbaar)
|
||||||
|
.OrderByDescending(r => r.Datum).ThenBy(r => r.Id).ToListAsync(ct);
|
||||||
|
|
||||||
|
public Task<VersieRegel?> FindAsync(int id, CancellationToken ct = default) =>
|
||||||
|
_db.VersieRegels.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||||
|
|
||||||
|
public Task<bool> BestaatAsync(DateOnly datum, string kop, int? behalveId, CancellationToken ct = default) =>
|
||||||
|
_db.VersieRegels.AnyAsync(r => r.Datum == datum && r.Kop == kop && (behalveId == null || r.Id != behalveId), ct);
|
||||||
|
|
||||||
|
public void Add(VersieRegel regel) => _db.VersieRegels.Add(regel);
|
||||||
|
public void Remove(VersieRegel regel) => _db.VersieRegels.Remove(regel);
|
||||||
|
}
|
||||||
|
|
||||||
public class VaultRepository : IVaultRepository
|
public class VaultRepository : IVaultRepository
|
||||||
{
|
{
|
||||||
private readonly MijnBesluitDbContext _db;
|
private readonly MijnBesluitDbContext _db;
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ public static class SocketActions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string VaultCategory = "vault.category";
|
public const string VaultCategory = "vault.category";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// De versiehistorie: de zichtbare regels, nieuwste dag bovenaan. Staat in een tabel op de
|
||||||
|
/// server, zodat een nieuwe regel zonder nieuwe app te zien is. Zie docs/Versiehistorie.md.
|
||||||
|
/// </summary>
|
||||||
|
public const string Versiehistorie = "app.versiehistorie";
|
||||||
|
|
||||||
/// <summary>Nalatenschappen die aan jou zijn toevertrouwd, en wat daarvan is vrijgegeven.</summary>
|
/// <summary>Nalatenschappen die aan jou zijn toevertrouwd, en wat daarvan is vrijgegeven.</summary>
|
||||||
public const string LegacyEntrusted = "legacy.entrusted";
|
public const string LegacyEntrusted = "legacy.entrusted";
|
||||||
public const string LegacyEntrustedItems = "legacy.entrusted-items";
|
public const string LegacyEntrustedItems = "legacy.entrusted-items";
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public class RequestScope
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public VaultCategoryService? Soorten { get; init; }
|
public VaultCategoryService? Soorten { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Optioneel: zonder deze dienst geeft de versiehistorie een lege lijst.</summary>
|
||||||
|
public VersiehistorieService? Versiehistorie { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Voor het samenvoegen van dubbele profielen. Optioneel: zonder deze dienst werkt alles,
|
/// Voor het samenvoegen van dubbele profielen. Optioneel: zonder deze dienst werkt alles,
|
||||||
/// alleen wordt een tweede uitnodiging dan geweigerd in plaats van samengevoegd.
|
/// alleen wordt een tweede uitnodiging dan geweigerd in plaats van samengevoegd.
|
||||||
@@ -493,6 +496,14 @@ public class SocketDispatcher
|
|||||||
return SocketResponse.Ok(id, lijst);
|
return SocketResponse.Ok(id, lijst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SocketActions.Versiehistorie:
|
||||||
|
{
|
||||||
|
var lijst = scope.Versiehistorie is null
|
||||||
|
? Array.Empty<VersieRegelWeergave>()
|
||||||
|
: (await scope.Versiehistorie.ZichtbaarAsync(ct)).ToArray();
|
||||||
|
return SocketResponse.Ok(id, lijst);
|
||||||
|
}
|
||||||
|
|
||||||
case SocketActions.VaultOpen:
|
case SocketActions.VaultOpen:
|
||||||
{
|
{
|
||||||
var p = request.PayloadAs<IdPayload>();
|
var p = request.PayloadAs<IdPayload>();
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class SocketServerService : BackgroundService
|
|||||||
Families = scope.ServiceProvider.GetRequiredService<FamilyService>(),
|
Families = scope.ServiceProvider.GetRequiredService<FamilyService>(),
|
||||||
Vault = scope.ServiceProvider.GetRequiredService<VaultService>(),
|
Vault = scope.ServiceProvider.GetRequiredService<VaultService>(),
|
||||||
Soorten = scope.ServiceProvider.GetRequiredService<VaultCategoryService>(),
|
Soorten = scope.ServiceProvider.GetRequiredService<VaultCategoryService>(),
|
||||||
|
Versiehistorie = scope.ServiceProvider.GetRequiredService<VersiehistorieService>(),
|
||||||
Messages = scope.ServiceProvider.GetRequiredService<MessageService>(),
|
Messages = scope.ServiceProvider.GetRequiredService<MessageService>(),
|
||||||
Legacy = scope.ServiceProvider.GetRequiredService<LegacyService>(),
|
Legacy = scope.ServiceProvider.GetRequiredService<LegacyService>(),
|
||||||
Meldingen = scope.ServiceProvider.GetRequiredService<MeldingService>(),
|
Meldingen = scope.ServiceProvider.GetRequiredService<MeldingService>(),
|
||||||
|
|||||||
@@ -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
|
De regels staan in een tabel op de server (version_history) en worden beheerd op de website
|
||||||
het overzicht, in de app hetzelfde. Dit bestand staat twee keer op schijf - de app en de site
|
onder Beheer > Versiehistorie. Site en app lezen dezelfde tabel; dit bestand bevat zelf geen
|
||||||
delen geen project - en een zelfcontrole vergelijkt ze letterlijk.
|
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. *@
|
"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">
|
<p class="zacht" style="margin-top:0">
|
||||||
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 style="margin-top:0">20 oktober 2026 · versie 1.1</h2>
|
@if (Fout)
|
||||||
<ul style="padding-left:1.2rem">
|
{
|
||||||
<li>
|
<p class="zacht">De versiehistorie kon niet worden opgehaald. Probeer het later nog eens.</p>
|
||||||
<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
|
else if (Regels is null)
|
||||||
daarvoor opnieuw hoeft te updaten. Je bestaande kluisitems blijven zoals ze waren.
|
{
|
||||||
</li>
|
<p class="zacht">Bezig met laden…</p>
|
||||||
</ul>
|
}
|
||||||
|
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>
|
@code {
|
||||||
<ul style="padding-left:1.2rem">
|
private IReadOnlyList<VersieRegelWeergave>? Regels;
|
||||||
<li style="margin-bottom:.5rem">
|
private bool Fout;
|
||||||
<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>
|
protected override async Task OnInitializedAsync()
|
||||||
<ul style="padding-left:1.2rem">
|
{
|
||||||
<li style="margin-bottom:.5rem">
|
try { Regels = await Historie.ZichtbaarAsync(); }
|
||||||
<strong>Met mij gedeeld, ook in de app.</strong> Heeft iemand een kluisitem rechtstreeks
|
catch { Fout = true; }
|
||||||
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>
|
private sealed record Groep(string Kop, bool Eerste, List<VersieRegelWeergave> Regels);
|
||||||
<li style="margin-bottom:.5rem">
|
|
||||||
<strong>Een gedeeld of nagelaten kluisitem bekijken.</strong> Dat opent nu als pop-up in
|
/// <summary>
|
||||||
dezelfde opmaak als het invulformulier, inclusief de foto's en documenten die erbij
|
/// Opeenvolgende regels met dezelfde dag (en dezelfde versie) vormen samen één kop. De volgorde
|
||||||
horen.
|
/// komt van de server: nieuwste dag bovenaan, binnen een dag in de volgorde van invoer.
|
||||||
</li>
|
/// </summary>
|
||||||
<li>
|
private IEnumerable<Groep> Groepen()
|
||||||
<strong>Met de vinger naar beneden trekken ververst het scherm.</strong> Werkt op Kluis,
|
{
|
||||||
Mijn mensen, Momenten en Nalatenschap.
|
var uit = new List<Groep>();
|
||||||
</li>
|
string? vorige = null;
|
||||||
</ul>
|
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. *@
|
Ook een eigen scherm: dat is inrichten, geen terugdraaien. *@
|
||||||
<p><a href="/beheer/soorten">Kluissoorten beheren ›</a></p>
|
<p><a href="/beheer/soorten">Kluissoorten beheren ›</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 ›</a></p>
|
||||||
|
|
||||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||||
|
|
||||||
@* ------------------------------------------------------------ statistieken *@
|
@* ------------------------------------------------------------ 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">‹ 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…</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> · 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>
|
</Popup>
|
||||||
}
|
}
|
||||||
|
|
||||||
@* Zelfde tekst als in de app: Versiehistorie.razor staat twee keer op schijf en een
|
@* De tekst komt uit de tabel version_history, net als in de app. *@
|
||||||
zelfcontrole vergelijkt ze letterlijk. *@
|
|
||||||
@if (VersiehistorieOpen)
|
@if (VersiehistorieOpen)
|
||||||
{
|
{
|
||||||
<Popup Titel="Versiehistorie" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
<Popup Titel="Versiehistorie" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class Store
|
|||||||
public List<CampagneUur> CampagneUren = new();
|
public List<CampagneUur> CampagneUren = new();
|
||||||
public List<VaultCategoryDefinition> Soorten = VaultCategorySaatgoed.Maak();
|
public List<VaultCategoryDefinition> Soorten = VaultCategorySaatgoed.Maak();
|
||||||
public List<VaultCategoryField> SoortVelden = new();
|
public List<VaultCategoryField> SoortVelden = new();
|
||||||
|
public List<VersieRegel> VersieRegels = new();
|
||||||
|
|
||||||
private long _seq;
|
private long _seq;
|
||||||
public long NextId() => ++_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>
|
/// <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 class VaultCategorySaatgoed
|
||||||
{
|
{
|
||||||
public static List<VaultCategoryDefinition> Maak()
|
public static List<VaultCategoryDefinition> Maak()
|
||||||
|
|||||||
@@ -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
|
// 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
|
// versies daarvan is het ergste geval van allemaal - het is het document waar iemand zich op
|
||||||
// beroept, en waarvan Google Play een adres wil.
|
// 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 web = Path.Combine(map.FullName, "src", "MijnBesluit.Web", "Components", "Onderdelen", naam);
|
||||||
var app = Path.Combine(map.FullName, "src", "MijnBesluit.AppUi", "Onderdelen", naam);
|
var app = Path.Combine(map.FullName, "src", "MijnBesluit.AppUi", "Onderdelen", naam);
|
||||||
@@ -1943,5 +1943,6 @@ ServiceChecks.Register(runner);
|
|||||||
AdvertentieChecks.Register(runner);
|
AdvertentieChecks.Register(runner);
|
||||||
UitrolChecks.Register(runner);
|
UitrolChecks.Register(runner);
|
||||||
SoortenChecks.Register(runner);
|
SoortenChecks.Register(runner);
|
||||||
|
VersiehistorieChecks.Register(runner);
|
||||||
|
|
||||||
return runner.Summary();
|
return runner.Summary();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class World
|
|||||||
public AdvertentieService Adverteren { get; }
|
public AdvertentieService Adverteren { get; }
|
||||||
public AccountOpheffenService Opheffen { get; }
|
public AccountOpheffenService Opheffen { get; }
|
||||||
public VaultCategoryService Soorten { 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>
|
/// <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 = "")
|
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,
|
Families = new FamilyService(families, people, users, invitations, messages, legacy,
|
||||||
Email, audit, uow, Clock);
|
Email, audit, uow, Clock);
|
||||||
Soorten = new VaultCategoryService(new VaultCategoryRepo(Store), users, 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);
|
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);
|
Meldingen = new MeldingService(new ToestelRepo(Store), users, Push, uow, Clock);
|
||||||
Messages = new MessageService(messages, people, users, vault, keys, files, Email, audit, uow, Clock, Meldingen);
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user