Compare commits
9
Commits
979dce4dbf
...
90c58fc42e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90c58fc42e | ||
|
|
ae7b30b745 | ||
|
|
4bf1ee1060 | ||
|
|
fe7ad9e97b | ||
|
|
af13d0e533 | ||
|
|
a3a0bcfc05 | ||
|
|
3b7f0412b6 | ||
|
|
28d96a483b | ||
|
|
7b60d7583e |
@@ -0,0 +1,205 @@
|
||||
<#
|
||||
Zet de nieuwste AAB in de Google Play Console (Android Publisher API), met versiecontrole.
|
||||
Voor SideKick: onderdeel mijnbesluit-playstore, Sleutel playstore. NIET GETEST tegen Google.
|
||||
|
||||
Wat het doet
|
||||
1. Leest ApplicationVersion uit MijnBesluit.App.csproj en vraagt aan Google welke versiecodes
|
||||
al op de tracks en in de bundels staan. Is de versie niet hoger dan de hoogste: stoppen.
|
||||
2. Uploadt de nieuwste *.aab uit -AabMap.
|
||||
3. Zet hem op de track (standaard intern testen) als CONCEPT: er gaat niets live. Publiceren
|
||||
doe je zelf in de Play Console.
|
||||
4. Schrijft playstore-upload.txt in -Uitvoer (zo ziet SideKick dat het gelukt is).
|
||||
|
||||
Eenmalig regelen (zie docs\Android-app.md)
|
||||
* De app staat al in de Play Console en de eerste AAB is met de hand geupload.
|
||||
* Een serviceaccount met sleutelbestand (JSON) en recht om releases te beheren.
|
||||
* Gebruikersvariabele MIJNBESLUIT_PLAY_SLEUTEL = pad naar dat JSON-bestand (buiten de repo).
|
||||
SideKick opnieuw starten na het zetten van de variabele.
|
||||
|
||||
Het sleutelbestand wordt alleen gelezen en komt nooit in een log of de repo.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Configuratie = 'Release',
|
||||
|
||||
# Waar playstore-upload.txt komt (SideKick: <publicatiemap>\playstore).
|
||||
[string] $Uitvoer,
|
||||
|
||||
# De map met de AAB van Bouwen-android-aab.ps1.
|
||||
[string] $AabMap = 'D:\MijnBesluit VS\MijnBesluitpublish\android-aab',
|
||||
|
||||
[string] $Pakket = 'nl.mijnbesluit.app',
|
||||
|
||||
# internal = intern testen, alpha = gesloten testen, production = productie.
|
||||
[ValidateSet('internal', 'alpha', 'beta', 'production')]
|
||||
[string] $Track = 'internal',
|
||||
|
||||
# draft = concept, nog te publiceren in de Play Console. completed = meteen uitrollen
|
||||
# (bij een app die nog nooit is gepubliceerd weigert Google dat).
|
||||
[ValidateSet('draft', 'completed')]
|
||||
[string] $Status = 'draft',
|
||||
|
||||
[string] $Notities = 'Nieuwe versie.',
|
||||
[string] $Taal = 'nl-NL',
|
||||
|
||||
# Alleen controleren (versies ophalen en vergelijken); niets uploaden.
|
||||
[switch] $Controle
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$csproj = Join-Path $PSScriptRoot '..\src\MijnBesluit.App\MijnBesluit.App.csproj' | Resolve-Path
|
||||
|
||||
# ------------------------------------------------------------------ versie uit de csproj
|
||||
|
||||
$tekst = Get-Content $csproj -Raw
|
||||
if ($tekst -notmatch '<ApplicationVersion>\s*(\d+)\s*</ApplicationVersion>') {
|
||||
throw "Geen ApplicationVersion (geheel getal) gevonden in $csproj."
|
||||
}
|
||||
$nieuweVersie = [int]$Matches[1]
|
||||
$weergave = if ($tekst -match '<ApplicationDisplayVersion>\s*([^<]+?)\s*</ApplicationDisplayVersion>') { $Matches[1] } else { '?' }
|
||||
Write-Host "Versie in de csproj: $weergave (versiecode $nieuweVersie)"
|
||||
|
||||
# ------------------------------------------------------------------ welke AAB
|
||||
|
||||
if (-not (Test-Path $AabMap)) { throw "De AAB-map bestaat niet: $AabMap. Bouw eerst de AAB (mijnbesluit-aab)." }
|
||||
$aab = Get-ChildItem $AabMap -Filter '*.aab' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $aab) { throw "Geen .aab gevonden in $AabMap. Bouw eerst de AAB (mijnbesluit-aab)." }
|
||||
Write-Host "AAB: $($aab.FullName) ($([math]::Round($aab.Length / 1MB, 1)) MB, $($aab.LastWriteTime))"
|
||||
|
||||
if ($aab.LastWriteTime -lt (Get-Item $csproj).LastWriteTime) {
|
||||
throw "De AAB is ouder dan de csproj. Bouw de AAB opnieuw (mijnbesluit-aab), anders zit er een andere versiecode in dan hierboven."
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ sleutelbestand en token
|
||||
|
||||
$sleutelPad = $env:MIJNBESLUIT_PLAY_SLEUTEL
|
||||
if (-not $sleutelPad) {
|
||||
throw @"
|
||||
De gebruikersvariabele MIJNBESLUIT_PLAY_SLEUTEL ontbreekt.
|
||||
Zet hem op het pad van het JSON-sleutelbestand van het serviceaccount (buiten de repo):
|
||||
[Environment]::SetEnvironmentVariable('MIJNBESLUIT_PLAY_SLEUTEL', 'D:\pad\naar\sleutel.json', 'User')
|
||||
Start SideKick daarna opnieuw.
|
||||
"@
|
||||
}
|
||||
if (-not (Test-Path $sleutelPad)) { throw "Het sleutelbestand bestaat niet: $sleutelPad" }
|
||||
$sa = Get-Content $sleutelPad -Raw | ConvertFrom-Json
|
||||
if (-not $sa.client_email -or -not $sa.private_key) { throw "Dit is geen sleutelbestand van een serviceaccount (client_email/private_key ontbreken)." }
|
||||
|
||||
function ConvertTo-Base64Url([byte[]] $bytes) {
|
||||
[Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
|
||||
}
|
||||
|
||||
function Get-Token {
|
||||
$nu = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
$kop = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes('{"alg":"RS256","typ":"JWT"}'))
|
||||
$aud = if ($sa.token_uri) { $sa.token_uri } else { 'https://oauth2.googleapis.com/token' }
|
||||
$claims = @{ iss = $sa.client_email; scope = 'https://www.googleapis.com/auth/androidpublisher'; aud = $aud; iat = $nu; exp = $nu + 3000 } | ConvertTo-Json -Compress
|
||||
$inhoud = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($claims))
|
||||
$teTekenen = [Text.Encoding]::ASCII.GetBytes("$kop.$inhoud")
|
||||
|
||||
$pem = ($sa.private_key -replace '-----[A-Z ]+-----', '' -replace '\s', '')
|
||||
$sleutel = [Security.Cryptography.CngKey]::Import([Convert]::FromBase64String($pem), [Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
|
||||
$rsa = New-Object Security.Cryptography.RSACng($sleutel)
|
||||
$handtekening = $rsa.SignData($teTekenen, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
|
||||
$jwt = "$kop.$inhoud." + (ConvertTo-Base64Url $handtekening)
|
||||
|
||||
$antwoord = Invoke-RestMethod -Method Post -Uri $aud -ContentType 'application/x-www-form-urlencoded' `
|
||||
-Body "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=$jwt"
|
||||
return $antwoord.access_token
|
||||
}
|
||||
|
||||
function Invoke-Play([string] $methode, [string] $url, $body = $null, [string] $infile = $null) {
|
||||
$kopjes = @{ Authorization = "Bearer $script:token" }
|
||||
try {
|
||||
if ($infile) {
|
||||
return Invoke-RestMethod -Method $methode -Uri $url -Headers $kopjes -ContentType 'application/octet-stream' -InFile $infile -TimeoutSec 900
|
||||
}
|
||||
if ($null -ne $body) {
|
||||
return Invoke-RestMethod -Method $methode -Uri $url -Headers $kopjes -ContentType 'application/json' -Body ($body | ConvertTo-Json -Depth 10)
|
||||
}
|
||||
return Invoke-RestMethod -Method $methode -Uri $url -Headers $kopjes
|
||||
}
|
||||
catch {
|
||||
$detail = $_.ErrorDetails.Message
|
||||
if (-not $detail -and $_.Exception.Response) {
|
||||
try { $detail = (New-Object IO.StreamReader($_.Exception.Response.GetResponseStream())).ReadToEnd() } catch { }
|
||||
}
|
||||
throw "Google Play weigerde $methode $url : $($_.Exception.Message) $detail"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Inloggen bij Google Play als $($sa.client_email)..."
|
||||
$token = Get-Token
|
||||
|
||||
$basis = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$Pakket"
|
||||
$edit = Invoke-Play 'POST' "$basis/edits" @{}
|
||||
$editId = $edit.id
|
||||
Write-Host "Bewerking $editId geopend."
|
||||
|
||||
try {
|
||||
# -------------------------------------------------------------- versiecontrole
|
||||
$hoogste = 0
|
||||
$gevonden = @()
|
||||
$tracks = Invoke-Play 'GET' "$basis/edits/$editId/tracks"
|
||||
foreach ($t in @($tracks.tracks)) {
|
||||
foreach ($r in @($t.releases)) {
|
||||
foreach ($v in @($r.versionCodes)) {
|
||||
$gevonden += "$($t.track): $v ($($r.status))"
|
||||
if ([int]$v -gt $hoogste) { $hoogste = [int]$v }
|
||||
}
|
||||
}
|
||||
}
|
||||
$bundels = Invoke-Play 'GET' "$basis/edits/$editId/bundles"
|
||||
foreach ($b in @($bundels.bundles)) {
|
||||
if ([int]$b.versionCode -gt $hoogste) { $hoogste = [int]$b.versionCode }
|
||||
}
|
||||
if ($gevonden.Count) { Write-Host "Al bij Google: $($gevonden -join '; ')" } else { Write-Host "Nog geen versies bij Google gevonden." }
|
||||
Write-Host "Hoogste versiecode bij Google: $hoogste; nieuwe versiecode: $nieuweVersie"
|
||||
|
||||
if ($nieuweVersie -le $hoogste) {
|
||||
throw "De versiecode in de csproj ($nieuweVersie) is niet hoger dan wat Google al heeft ($hoogste). Verhoog ApplicationVersion in MijnBesluit.App.csproj en bouw de AAB opnieuw."
|
||||
}
|
||||
|
||||
if ($Controle) {
|
||||
Write-Host "Controle gelukt: versiecode $nieuweVersie mag geupload worden. Er is niets geupload." -ForegroundColor Green
|
||||
Invoke-Play 'DELETE' "$basis/edits/$editId" | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------- uploaden
|
||||
Write-Host "AAB uploaden..."
|
||||
$bundel = Invoke-Play 'POST' "https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications/$Pakket/edits/$editId/bundles?uploadType=media" -infile $aab.FullName
|
||||
$code = [int]$bundel.versionCode
|
||||
Write-Host "Geupload als versiecode $code."
|
||||
if ($code -ne $nieuweVersie) {
|
||||
Write-Host "Let op: de AAB heeft versiecode $code, de csproj zegt $nieuweVersie. Bouw de AAB opnieuw als dat niet de bedoeling is." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------- op de track zetten
|
||||
$release = @{
|
||||
name = "$weergave ($code)"
|
||||
versionCodes = @("$code")
|
||||
status = $Status
|
||||
releaseNotes = @(@{ language = $Taal; text = $Notities })
|
||||
}
|
||||
Invoke-Play 'PUT' "$basis/edits/$editId/tracks/$Track" @{ track = $Track; releases = @($release) } | Out-Null
|
||||
Write-Host "Op track '$Track' gezet (status: $Status)."
|
||||
|
||||
Invoke-Play 'POST' "$basis/edits/$editId`:commit" @{} | Out-Null
|
||||
Write-Host "Vastgelegd bij Google Play." -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
try { Invoke-Play 'DELETE' "$basis/edits/$editId" | Out-Null } catch { }
|
||||
throw
|
||||
}
|
||||
|
||||
$samenvatting = "Geupload naar Google Play ($Pakket): versie $weergave, versiecode $code, track $Track, status $Status, op $(Get-Date -Format 'yyyy-MM-dd HH:mm')."
|
||||
if ($Uitvoer) {
|
||||
New-Item -ItemType Directory -Path $Uitvoer -Force | Out-Null
|
||||
Set-Content -Path (Join-Path $Uitvoer 'playstore-upload.txt') -Value $samenvatting -Encoding UTF8
|
||||
}
|
||||
Write-Host $samenvatting
|
||||
if ($Status -eq 'draft') { Write-Host "Het is een concept: publiceer de release zelf in de Play Console." }
|
||||
@@ -53,6 +53,8 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`adverteerders` TO 'm
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`campagnes` TO 'mijnbesluit_app'@'localhost';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`campagne_uren` 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_category_fields` TO 'mijnbesluit_app'@'localhost';
|
||||
|
||||
-- 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';
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
-- Wie een bezorgd bericht "verwijdert" haalt het alleen bij zichzelf weg; de ander heeft er nog
|
||||
-- steeds recht op. Vandaar twee vlaggen in plaats van één verwijderdatum: de afzender en de
|
||||
-- ontvanger ruimen onafhankelijk van elkaar op. De rij zelf blijft altijd bestaan.
|
||||
ALTER TABLE `messages`
|
||||
ADD COLUMN `hidden_by_sender` TINYINT(1) NOT NULL DEFAULT 0 AFTER `read_at`,
|
||||
ADD COLUMN `hidden_by_recipient` TINYINT(1) NOT NULL DEFAULT 0 AFTER `hidden_by_sender`;
|
||||
--
|
||||
-- Draaien met:
|
||||
-- mysql -u root -p mijnbesluit < db\20_bericht_verbergen.sql
|
||||
--
|
||||
-- Opnieuw draaien mag: er gebeurt dan niets. (De uitroller draait alle scripts elke keer; een
|
||||
-- script dat alleen "ADD COLUMN" zegt, loopt de tweede keer vast en houdt de rest tegen.)
|
||||
|
||||
USE `mijnbesluit`;
|
||||
|
||||
-- ---------------------------------------------------------------- hidden_by_sender
|
||||
SET @bestaat := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'hidden_by_sender');
|
||||
|
||||
SET @sql := IF(@bestaat = 0,
|
||||
'ALTER TABLE `messages` ADD COLUMN `hidden_by_sender` TINYINT(1) NOT NULL DEFAULT 0 AFTER `read_at`',
|
||||
'SELECT "hidden_by_sender bestaat al" AS melding');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---------------------------------------------------------------- hidden_by_recipient
|
||||
SET @bestaat := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'hidden_by_recipient');
|
||||
|
||||
SET @sql := IF(@bestaat = 0,
|
||||
'ALTER TABLE `messages` ADD COLUMN `hidden_by_recipient` TINYINT(1) NOT NULL DEFAULT 0 AFTER `hidden_by_sender`',
|
||||
'SELECT "hidden_by_recipient bestaat al" AS melding');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT 'Klaar. Een bezorgd bericht is vanaf nu uit je eigen lijst te verwijderen.' AS melding;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
-- MijnBesluit - kluissoorten uit de database in plaats van uit de code
|
||||
--
|
||||
-- Tot nu toe stond de lijst "Wachtwoord, Polis, Uitvaartwens, ..." vast in de code, met per soort
|
||||
-- een eigen invulscherm. Elke nieuwe soort (denk aan "Dementie" of "Niet reanimeren") vroeg dus
|
||||
-- een nieuwe versie van de app in de Play Store en de App Store. Nu staan de soorten hier, en de
|
||||
-- velden van een nieuwe soort ook: de beheerder onderhoudt ze op de website (/beheer/soorten) en
|
||||
-- de app tekent het formulier naar wat de server beschrijft.
|
||||
--
|
||||
-- vault_categories de soorten. De elf ingebouwde en "Overig" (nummer 99) krijgen hier
|
||||
-- hun startrij; nieuwe soorten beginnen bij nummer 100.
|
||||
-- vault_category_fields de velden van een nieuwe soort. Ingebouwde soorten hebben hier geen
|
||||
-- rijen: hun invulscherm zit nog in de app zelf.
|
||||
--
|
||||
-- Regels die de server afdwingt (niet de database):
|
||||
-- * Een soort waar kluisitems mee zijn gemaakt kan niet worden verwijderd, alleen op niet-actief
|
||||
-- gezet. Niet-actief verdwijnt uit de keuzelijst van wie er nog nooit een item mee maakte;
|
||||
-- wie dat wel deed, blijft de soort zien.
|
||||
-- * Nieuwe items kunnen alleen in een actieve soort, of in een soort waar de eigenaar al een
|
||||
-- item van heeft.
|
||||
--
|
||||
-- vault_items.category blijft een gewoon getal; er komt bewust geen foreign key bij, zodat
|
||||
-- bestaande gegevens en oudere apps niet in de weg zitten.
|
||||
--
|
||||
-- Draaien met:
|
||||
-- mysql -u root -p mijnbesluit < db\21_kluissoorten.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 tabellen)
|
||||
-- mysql -u root -p mijnbesluit < db\handmatig\21b_kluissoorten_startrijen.sql (de startrijen)
|
||||
|
||||
USE `mijnbesluit`;
|
||||
|
||||
-- ---------------------------------------------------------------- vault_categories
|
||||
CREATE TABLE IF NOT EXISTS `vault_categories` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`naam` VARCHAR(80) NOT NULL,
|
||||
`uitleg` VARCHAR(500) NULL, -- verschijnt onder de keuzelijst in de app
|
||||
`actief` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`volgorde` INT NOT NULL DEFAULT 1000,
|
||||
`bespreken_met` TINYINT(1) NOT NULL DEFAULT 0, -- mag de eigenaar een beheerder kiezen om vooraf mee te bespreken?
|
||||
`ingebouwd` TINYINT(1) NOT NULL DEFAULT 0, -- 1 = invulscherm zit in de app; velden hier niet te wijzigen
|
||||
`aangemaakt_op` DATETIME(3) NOT NULL,
|
||||
`gewijzigd_op` DATETIME(3) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `ux_vault_categories_naam` (`naam`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=100;
|
||||
|
||||
-- ---------------------------------------------------------------- vault_category_fields
|
||||
CREATE TABLE IF NOT EXISTS `vault_category_fields` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`category_id` INT NOT NULL,
|
||||
`sleutel` VARCHAR(40) NOT NULL, -- vaste naam in de versleutelde inhoud; wijzigt nooit
|
||||
`label` VARCHAR(100) NOT NULL,
|
||||
`soort` VARCHAR(10) NOT NULL DEFAULT 'tekst', -- tekst | lang | datum | keuze | uitleg
|
||||
`opties` VARCHAR(1000) NULL, -- bij 'keuze': één optie per regel
|
||||
`hulptekst` VARCHAR(300) NULL,
|
||||
`verplicht` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`actief` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`volgorde` INT NOT NULL DEFAULT 1000,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `ux_vault_category_fields_sleutel` (`category_id`, `sleutel`),
|
||||
CONSTRAINT `fk_vcf_category` FOREIGN KEY (`category_id`) REFERENCES `vault_categories` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ---------------------------------------------------------------- startrijen
|
||||
-- De startrijen (de elf ingebouwde soorten en Overig) staan NIET hier maar in
|
||||
-- handmatig\21b_kluissoorten_startrijen.sql.
|
||||
--
|
||||
-- Waarom: dit script wordt ook door de uitrolagent gedraaid, met een beperkt databaseaccount dat
|
||||
-- tabellen mag maken maar er geen gegevens in mag zetten. Een INSERT hier laat de hele uitrol
|
||||
-- struikelen (ERROR 1142: INSERT command denied). Gegevens neerzetten is inrichten, en dat doet een
|
||||
-- mens één keer als root, net als de rechten in 02_applicatiegebruiker.sql.
|
||||
|
||||
SELECT 'Klaar. De tabellen voor kluissoorten staan klaar. Vergeet handmatig\\21b_kluissoorten_startrijen.sql en 02_applicatiegebruiker.sql niet (als root).' AS melding;
|
||||
@@ -0,0 +1,36 @@
|
||||
-- MijnBesluit - startrijen voor de kluissoorten (eenmalig, als root)
|
||||
--
|
||||
-- Hoort bij 21_kluissoorten.sql, dat de tabellen maakt. Dit script zet de twaalf soorten erin die
|
||||
-- er altijd al waren: de elf ingebouwde en "Overig" (nummer 99). De nummers zijn die van
|
||||
-- VaultCategory in de code en van vault_items.category; wijzig ze nooit.
|
||||
--
|
||||
-- Dit staat in de submap handmatig\ en niet in db\ zelf: de uitrolagent draait alles in db\, met een
|
||||
-- beperkt account dat geen gegevens mag invoegen. Zie de kop van 21_kluissoorten.sql.
|
||||
--
|
||||
-- Draaien met (in cmd, NIET in PowerShell):
|
||||
-- mysql -u root -p mijnbesluit < db\handmatig\21b_kluissoorten_startrijen.sql
|
||||
--
|
||||
-- Opnieuw draaien mag: bestaande soorten blijven zoals ze zijn (ook als je er een hernoemd of
|
||||
-- op niet-actief gezet hebt).
|
||||
|
||||
USE `mijnbesluit`;
|
||||
|
||||
-- De volgorde is die van de keuzelijst in de app. Alleen uitvaartwens en donorcodicil kennen
|
||||
-- "Bespreken met", precies zoals voorheen.
|
||||
INSERT IGNORE INTO `vault_categories`
|
||||
(`id`, `naam`, `actief`, `volgorde`, `bespreken_met`, `ingebouwd`, `aangemaakt_op`, `gewijzigd_op`) VALUES
|
||||
( 3, 'Wachtwoord', 1, 10, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 8, 'Sociale media', 1, 20, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 4, 'Apparaat', 1, 30, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 5, 'Pincode', 1, 40, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 2, 'Polis', 1, 50, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 1, 'Uitvaartwens', 1, 60, 1, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 9, 'Donorcodicil', 1, 70, 1, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
(10, 'Rijbewijs', 1, 80, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
(11, 'Paspoort', 1, 90, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 6, 'Document', 1, 100, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
( 7, 'Notitie', 1, 110, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)),
|
||||
(99, 'Overig', 1, 120, 0, 1, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3));
|
||||
|
||||
-- Controle: dit hoort twaalf te zijn (of meer, als de beheerder er al een bij zette).
|
||||
SELECT COUNT(*) AS aantal_soorten FROM `vault_categories`;
|
||||
@@ -324,3 +324,19 @@ meldingstoestemming wordt op het moment zelf gevraagd.
|
||||
6. **"Bevat advertenties" aanvinken** zodra de advertentiebalk meedraait. De inhoud van die
|
||||
advertenties valt dan onder het beleid van Google; dat is een reden te meer om ze vooraf zelf
|
||||
goed te keuren.
|
||||
|
||||
## Bouwen vanuit SideKick
|
||||
|
||||
In SideKick (project MijnBesluit) heet het onderdeel `mijnbesluit-app`, met Sleutel `android`. SideKick zoekt voor zo'n onderdeel eerst `tools\Bouwen-<Sleutel>.ps1`; dat is hier `tools\Bouwen-android.ps1`, dat `tools\Apk-maken.ps1` aanroept en de ondertekende APK in `D:\MijnBesluit VS\MijnBesluitpublish\android` zet. Zonder dat script bouwt SideKick een Windows-programma (win-x64), en dat faalt op een Android-project met `NU1102 ... Mono.win-x64`.
|
||||
|
||||
Het wachtwoord van het sleutelbestand komt uit de gebruikersvariabele `MIJNBESLUIT_KEYSTORE_WACHTWOORD`; start SideKick opnieuw als je die net hebt gezet. Voor een AAB (Play Store) draai je `Apk-maken.ps1 -Aab` zelf.
|
||||
|
||||
## Uploaden naar de Play Store vanuit SideKick (niet getest tegen Google)
|
||||
|
||||
SideKick heeft hiervoor een eigen Play Store-tegel bij Publiceren (het oude script `Bouwen-playstore.ps1` en de rij `mijnbesluit-playstore` zijn vervangen en niet meer nodig; het script staat in `_te_verwijderen`). Zie `Handleiding-voor-claude.md` van SideKick, hoofdstuk 6c.
|
||||
|
||||
Kort: bij het onderdeel `mijnbesluit-aab` staat in Instellingen het vinkje **Play** aan (Track leeg = intern testen). De koppeling met Google Play vul je eenmalig in bij Instellingen > Algemeen > **Winkelkoppelingen...** (inhoud van het JSON-bestand van het serviceaccount plakken of het bestand kiezen; het komt alleen in `secrets.json` buiten git; **Verbinding testen** controleert het). Bij Publiceren: de AAB bouwen, dan **Play-versie controleren** (`ApplicationVersion` moet hoger zijn dan de hoogste versiecode in de Play Console) en **Naar Play Store zetten**. De release komt als concept op de track; publiceren doe je in de Play Console.
|
||||
|
||||
**Release-opmerking:** zet de tekst voor de release in `docs\Play-release-opmerking.txt` (UTF-8, maximaal 500 tekens, in het Nederlands, bijvoorbeeld "Nieuwe kluissoorten, beheerbaar via de website"). Jonathan bekijkt en wijzigt hem in SideKick met de knop **Release-opmerking...**; hij gaat mee met de upload. Pas hem aan voor elke nieuwe versie.
|
||||
|
||||
Eenmalig: de app staat in de Play Console met een eerste, met de hand geuploade AAB; het serviceaccount is uitgenodigd onder Gebruikers en machtigingen met het recht om releases te beheren.
|
||||
|
||||
@@ -104,6 +104,7 @@ in plaats van dat er stilletjes onzin uit komt.
|
||||
Bewust leesbaar, omdat er anders niet te zoeken of te sorteren valt:
|
||||
|
||||
- de titel en de categorie van een kluisitem;
|
||||
- de naam van een kluissoort die de beheerder zelf maakte (zie [Kluissoorten](Kluissoorten.md));
|
||||
- het onderwerp, de afzender, de ontvanger en de bezorgdatum van een bericht;
|
||||
- namen, e-mailadressen en relaties.
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Kluissoorten
|
||||
|
||||
Wat iemand in zijn kluis kan vastleggen (Wachtwoord, Polis, Uitvaartwens, ... en alles wat de
|
||||
beheerder er zelf bij zet, zoals "Niet reanimeren" of "Dementie") staat in de database en niet in de
|
||||
code. Een nieuwe soort vraagt daarom **geen nieuwe versie van de app** in de Play Store of App Store.
|
||||
|
||||
## Hoe het werkt
|
||||
|
||||
* Tabel `vault_categories`: één rij per soort. Nummers 1-11 en 99 (Overig) zijn de ingebouwde
|
||||
soorten en houden hun oude nummer; soorten die de beheerder maakt beginnen bij 100.
|
||||
`vault_items.category` blijft een gewoon getal.
|
||||
* Tabel `vault_category_fields`: de velden van een eigen soort (label, soort veld, keuzes, hulptekst,
|
||||
verplicht, actief, volgorde).
|
||||
* De app en de website vragen de lijst op bij de server (`vault.categories`) en tekenen het
|
||||
formulier van een eigen soort naar zijn velden. Een ingebouwde soort heeft zijn invulscherm in de
|
||||
app zelf; die velden zijn niet te wijzigen. Wil je voor iets als "Polis" andere velden, maak dan
|
||||
een eigen soort.
|
||||
* De ingevulde waarden van een eigen soort staan versleuteld in `VaultPayload.Velden`, op de vaste
|
||||
`sleutel` van het veld. Een label hernoemen mag dus zonder dat iemands gegevens loskomen van hun veld.
|
||||
|
||||
## Regels
|
||||
|
||||
1. **Wat in gebruik is verdwijnt nooit.** Een soort met kluisitems kan niet worden verwijderd,
|
||||
alleen op niet-actief gezet. Een niet-actieve soort staat niet meer in de lijst van wie hem nog
|
||||
nooit gebruikte; wie er al een item van heeft, ziet hem nog en kan zijn items gewoon bekijken en
|
||||
wijzigen. Hetzelfde geldt voor een veld: met items eronder kan het alleen op niet-actief.
|
||||
2. **De beheerder ziet aantallen, nooit inhoud.** Per soort zie je hoeveel items en hoeveel mensen,
|
||||
niet wie en niet wat.
|
||||
3. **"Bespreken met" is een instelling per soort.** Startwaarden: aan bij Uitvaartwens en
|
||||
Donorcodicil, uit bij de rest.
|
||||
4. **De server controleert alles.** Een oudere of andere app stuurt wat hij wil: alleen bekende
|
||||
velden blijven over, verplichte velden moeten gevuld zijn en datums moeten kloppen.
|
||||
|
||||
## Privacy: de naam van een soort is leesbaar voor de server
|
||||
|
||||
Alleen de *inhoud* van een kluisitem is versleuteld. De titel en de soort staan leesbaar in de
|
||||
database, net als de naam die de beheerder aan een soort geeft. Een soort die "Dementieverklaring"
|
||||
heet, zet dat woord dus in de database en laat zien hoe vaak het gebruikt wordt. Kies een neutrale
|
||||
naam als de soort zelf iets over gezondheid zegt, en zet het gevoelige deel in de velden: die
|
||||
waarden blijven versleuteld. Het beheerscherm waarschuwt hiervoor.
|
||||
|
||||
## Beheren
|
||||
|
||||
Website, ingelogd als beheerder: **Beheer > Kluissoorten** (`/beheer/soorten`). Daar maak je een
|
||||
soort, hernoem je hem, zet je hem actief of niet-actief, kies je de plek in de lijst, zet je
|
||||
"bespreken met" aan of uit en beheer je de velden. Elke wijziging komt in het logboek
|
||||
(`KluisSoortBeheerd`).
|
||||
|
||||
## Uitrollen
|
||||
|
||||
1. **Database, eenmalig als root** (in cmd, niet in PowerShell). De uitrolagent heeft een beperkt
|
||||
account dat tabellen mag maken maar geen gegevens mag invoegen en geen rechten mag uitdelen;
|
||||
daarom zitten deze twee stappen niet in de uitrol zelf:
|
||||
|
||||
```
|
||||
mysql -u root -p < db\02_applicatiegebruiker.sql
|
||||
mysql -u root -p mijnbesluit < db\handmatig\21b_kluissoorten_startrijen.sql
|
||||
```
|
||||
|
||||
De eerste geeft de applicatiegebruiker rechten op de twee nieuwe tabellen (zonder die rechten
|
||||
krijgt de server "SELECT command denied"), de tweede zet de twaalf bestaande soorten erin.
|
||||
Beide mogen opnieuw gedraaid worden. Controle: de tweede meldt `aantal_soorten` 12.
|
||||
2. **Uitroller:** `21_kluissoorten.sql` maakt de tabellen (de uitrolagent draait dit mee),
|
||||
daarna komen de website en de SocketServer. Draai stap 1 vóór je uitrolt: de nieuwe server leest
|
||||
die tabellen meteen.
|
||||
3. Nieuwe versie van de app (verhoog `ApplicationVersion`) voor Android en iOS. Die versie kan het
|
||||
formulier van een eigen soort tekenen. Oudere versies blijven werken: een eigen soort toont
|
||||
daar als "Overig", de items zijn zichtbaar maar niet te wijzigen (de server weigert, zodat er niets
|
||||
van de ingevulde velden verloren gaat). Alleen de soorten zelf hoeven daarna nooit meer een release.
|
||||
|
||||
## Wat nog meer zonder release kan
|
||||
|
||||
Niet gebouwd, wel mogelijk op dezelfde manier: een mededelingenbalk in de app, een minimaal vereiste
|
||||
appversie, de lijst met sociale media en instelwaarden (bezwaartermijn, limieten).
|
||||
@@ -0,0 +1 @@
|
||||
Nieuw: de kluis ondersteunt nu meer soorten gegevens. Voortaan kunnen er nieuwe soorten bijkomen, met eigen invulvelden, zonder dat je daarvoor opnieuw de app hoeft te updaten. Je bestaande kluisitems blijven ongewijzigd. Verder kleine verbeteringen.
|
||||
@@ -74,6 +74,8 @@ aanvoelt. Bij `"Error"` staat in `message` een tekst die je zo aan de gebruiker
|
||||
| `vault.list` | ja | Titels en categorieën (geen inhoud) |
|
||||
| `vault.open` | ja | Eén item ontsleutelen |
|
||||
| `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.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 |
|
||||
| `message.inbox` / `message.outbox` | ja | Berichten voor en door jou |
|
||||
| `message.read` | ja | Bericht lezen en als gelezen markeren |
|
||||
| `message.compose` | ja | Bericht klaarzetten |
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
|
||||
<ApplicationTitle>MijnBesluit</ApplicationTitle>
|
||||
<ApplicationId>nl.mijnbesluit.app</ApplicationId>
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>2</ApplicationVersion>
|
||||
<ApplicationDisplayVersion>1.1</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>3</ApplicationVersion>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
|
||||
|
||||
|
||||
@@ -11,7 +11,16 @@
|
||||
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
||||
</p>
|
||||
|
||||
<h2 style="margin-top:0">20 september 2026</h2>
|
||||
<h2 style="margin-top:0">20 oktober 2026 · 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>
|
||||
|
||||
<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
|
||||
|
||||
@@ -140,22 +140,11 @@ public static class Tonen
|
||||
public static string Kort(DateOnly? dag) =>
|
||||
dag is null ? "" : $"{dag.Value.Day} {KorteMaanden[dag.Value.Month - 1]} {dag.Value.Year}";
|
||||
|
||||
/// <summary>De nummers komen overeen met VaultCategory in het domein.</summary>
|
||||
public static string Categorie(int categorie) => categorie switch
|
||||
{
|
||||
1 => "Uitvaartwens",
|
||||
2 => "Polis",
|
||||
3 => "Wachtwoord",
|
||||
4 => "Apparaat",
|
||||
5 => "Pincode",
|
||||
6 => "Document",
|
||||
7 => "Notitie",
|
||||
8 => "Sociale media",
|
||||
9 => "Donorcodicil",
|
||||
10 => "Rijbewijs",
|
||||
11 => "Paspoort",
|
||||
_ => "Overig"
|
||||
};
|
||||
/// <summary>
|
||||
/// De ingebouwde naam bij een soortnummer. Een soort die de beheerder zelf maakte staat hier niet
|
||||
/// in; die naam reist met het item mee (<see cref="MijnBesluit.Client.KluisItem.SoortNaam"/>).
|
||||
/// </summary>
|
||||
public static string Categorie(int categorie) => MijnBesluit.Client.IngebouwdeSoorten.Naam(categorie);
|
||||
|
||||
/// <summary>
|
||||
/// Hoe een relatie op het scherm heet. De lijst staat in MijnBesluit.Client, zodat de app en
|
||||
|
||||
@@ -22,18 +22,10 @@
|
||||
<div class="rij kluiskop">
|
||||
<select class="groei" aria-label="Filter op soort" disabled="@Bezig" @bind="Filter" @bind:after="LaadAsync">
|
||||
<option value="">Alles</option>
|
||||
<option value="3">Wachtwoord</option>
|
||||
<option value="8">Sociale media</option>
|
||||
<option value="4">Apparaat</option>
|
||||
<option value="5">Pincode</option>
|
||||
<option value="2">Polis</option>
|
||||
<option value="1">Uitvaartwens</option>
|
||||
<option value="9">Donorcodicil</option>
|
||||
<option value="10">Rijbewijs</option>
|
||||
<option value="11">Paspoort</option>
|
||||
<option value="6">Document</option>
|
||||
<option value="7">Notitie</option>
|
||||
<option value="99">Overig</option>
|
||||
@foreach (var soort in Soorten)
|
||||
{
|
||||
<option value="@soort.Id">@soort.Naam</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<button class="knop" disabled="@Bezig" @onclick="() => Nieuw()">Toevoegen</button>
|
||||
@@ -52,22 +44,19 @@
|
||||
|
||||
<div class="veld">
|
||||
<label for="soort">Wat leg je vast?</label>
|
||||
@* De nummers komen overeen met VaultCategory in het domein. Overig is 99, niet 9:
|
||||
dat stond hier eerder verkeerd en leverde een categorie op die nergens bij hoort. *@
|
||||
@* De lijst komt van de server: de soorten die de beheerder aanbiedt, plus de soorten
|
||||
waar je al een item van hebt. Kan de server er geen geven, dan staat hier de
|
||||
ingebouwde lijst (IngebouwdeSoorten), met dezelfde nummers. *@
|
||||
<select id="soort" @bind="Categorie">
|
||||
<option value="3">Wachtwoord</option>
|
||||
<option value="8">Sociale media</option>
|
||||
<option value="4">Apparaat</option>
|
||||
<option value="5">Pincode</option>
|
||||
<option value="2">Polis</option>
|
||||
<option value="1">Uitvaartwens</option>
|
||||
<option value="9">Donorcodicil</option>
|
||||
<option value="10">Rijbewijs</option>
|
||||
<option value="11">Paspoort</option>
|
||||
<option value="6">Document</option>
|
||||
<option value="7">Notitie</option>
|
||||
<option value="99">Overig</option>
|
||||
@foreach (var soort in Soorten)
|
||||
{
|
||||
<option value="@soort.Id">@soort.Naam</option>
|
||||
}
|
||||
</select>
|
||||
@if (!string.IsNullOrWhiteSpace(HuidigeSoort?.Uitleg))
|
||||
{
|
||||
<div class="uitleg">@HuidigeSoort!.Uitleg</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="veld">
|
||||
@@ -153,7 +142,17 @@
|
||||
break;
|
||||
}
|
||||
|
||||
@if (Categorie is 1 or 9)
|
||||
@* Een soort die de beheerder zelf maakte beschrijft zijn formulier op de server; hier wordt het
|
||||
getekend. Een ingebouwde soort heeft zijn velden hierboven in de switch. *@
|
||||
@if (HuidigeSoort is { Ingebouwd: false } eigen)
|
||||
{
|
||||
@foreach (var veld in eigen.Velden.Where(v => v.Actief || HeeftWaarde(v)))
|
||||
{
|
||||
@VeldInvoer(veld)
|
||||
}
|
||||
}
|
||||
|
||||
@if (HuidigeSoort?.BesprekenMet == true)
|
||||
{
|
||||
<div class="veld">
|
||||
<label for="bespreken">Bespreken met</label>
|
||||
@@ -278,7 +277,7 @@
|
||||
@onkeydown="e => ToetsAsync(e, () => TegelOpenenAsync(item))">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span class="groot">@Tonen.Categorie(item.Category)</span>
|
||||
<span class="groot">@item.SoortNaam(NaamVan)</span>
|
||||
<div class="zacht" title="@item.Title">@item.Title</div>
|
||||
|
||||
@* stopPropagation, want anders opent een druk op de prullenbak ook meteen
|
||||
@@ -342,7 +341,16 @@
|
||||
/// <summary>Op welk soort de lijst gefilterd staat; leeg is alles. Zelfde opzet als op de website.</summary>
|
||||
private string Filter = "";
|
||||
|
||||
/// <summary>De soorten in de keuzelijst; zie <see cref="LaadAsync"/>.</summary>
|
||||
private IReadOnlyList<KluisSoort> Soorten = IngebouwdeSoorten.Lijst;
|
||||
|
||||
private int Categorie = 3;
|
||||
|
||||
/// <summary>De soort die in het formulier gekozen is. Null als de lijst hem niet kent.</summary>
|
||||
private KluisSoort? HuidigeSoort => Soorten.FirstOrDefault(s => s.Id == Categorie);
|
||||
|
||||
/// <summary>De naam van een soort uit de lijst, of de ingebouwde naam als de lijst hem niet kent.</summary>
|
||||
private string NaamVan(int soort) => Soorten.FirstOrDefault(s => s.Id == soort)?.Naam ?? IngebouwdeSoorten.Naam(soort);
|
||||
private IReadOnlyList<Beheerder> Beheerders = Array.Empty<Beheerder>();
|
||||
private string BesprekenMetTekst = "";
|
||||
private long? BesprekenMetPersonId => long.TryParse(BesprekenMetTekst, out var p) ? p : null;
|
||||
@@ -383,6 +391,11 @@
|
||||
if (lijst.Gelukt) Items = lijst.Waarde!;
|
||||
else Fout = lijst.Melding;
|
||||
|
||||
// De soorten van de server. Geen lijst terug (een oudere server, of even geen verbinding):
|
||||
// dan blijft het de ingebouwde lijst, en werkt de kluis precies zoals voorheen.
|
||||
var soorten = await Toestand.Client.KluisSoortenAsync();
|
||||
Soorten = soorten.Gelukt && soorten.Waarde is { Count: > 0 } ? soorten.Waarde : IngebouwdeSoorten.Lijst;
|
||||
|
||||
var beheerders = await Toestand.Client.BeheerdersAsync();
|
||||
if (beheerders.Gelukt) Beheerders = beheerders.Waarde!;
|
||||
|
||||
@@ -405,7 +418,7 @@
|
||||
Bewerken = new KluisInhoud();
|
||||
ItemId = 0;
|
||||
Titel = "";
|
||||
Categorie = 3;
|
||||
Categorie = Soorten.Any(s => s.Id == 3) ? 3 : Soorten[0].Id;
|
||||
Vrijgave = 1;
|
||||
VrijgaveDatum = DateTime.Today.AddYears(1);
|
||||
BesprekenMetTekst = "";
|
||||
@@ -456,6 +469,15 @@
|
||||
var datum = Vrijgave == 2 ? VrijgaveDatum : null;
|
||||
if (Vrijgave == 2 && datum is null) { Fout = "Kies een datum waarop dit vrijkomt."; return; }
|
||||
|
||||
// Verplichte velden van een soort die de beheerder maakte. De server controleert het ook;
|
||||
// dit is om het meteen bij het veld te kunnen zeggen.
|
||||
if (HuidigeSoort is { Ingebouwd: false } eigen)
|
||||
{
|
||||
var leeg = eigen.Velden.FirstOrDefault(v => v.Actief && v.Verplicht && v.Soort != KluisVeld.Uitleg
|
||||
&& string.IsNullOrWhiteSpace(Waarde(v)));
|
||||
if (leeg is not null) { Fout = $"Vul in: {leeg.Label}."; return; }
|
||||
}
|
||||
|
||||
long nieuwItemId = 0;
|
||||
|
||||
if (await ProbeerAsync(async () =>
|
||||
@@ -592,6 +614,86 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ eigen velden
|
||||
|
||||
private string Waarde(KluisVeld veld) =>
|
||||
Bewerken is not null && Bewerken.Velden.TryGetValue(veld.Sleutel, out var w) ? w : "";
|
||||
|
||||
private bool HeeftWaarde(KluisVeld veld) => !string.IsNullOrWhiteSpace(Waarde(veld));
|
||||
|
||||
private void Zet(KluisVeld veld, string? waarde)
|
||||
{
|
||||
if (Bewerken is null) return;
|
||||
if (string.IsNullOrWhiteSpace(waarde)) Bewerken.Velden.Remove(veld.Sleutel);
|
||||
else Bewerken.Velden[veld.Sleutel] = waarde.Trim();
|
||||
}
|
||||
|
||||
/// <summary>De opgeslagen datum (jjjj-mm-dd) als datum voor de kiezer; null als er niets staat.</summary>
|
||||
private DateTime? DatumWaarde(KluisVeld veld) =>
|
||||
DateOnly.TryParseExact(Waarde(veld), "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None, out var dag)
|
||||
? dag.ToDateTime(TimeOnly.MinValue)
|
||||
: null;
|
||||
|
||||
private void ZetDatum(KluisVeld veld, DateTime? datum) =>
|
||||
Zet(veld, datum?.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
/// <summary>
|
||||
/// Eén veld van een door de beheerder gemaakte soort. De invoer gaat via Zet in plaats van @bind:
|
||||
/// de waarden staan in een woordenboek op de sleutel van het veld, en een veld dat leeg wordt
|
||||
/// gemaakt hoort er dan uit te verdwijnen in plaats van als lege tekst te blijven staan.
|
||||
/// </summary>
|
||||
private RenderFragment VeldInvoer(KluisVeld veld) => @<div class="veld">
|
||||
@if (veld.Soort == KluisVeld.Uitleg)
|
||||
{
|
||||
<div class="uitleg">@veld.Hulptekst</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label for="@VeldId(veld)">@veld.Label@(veld.Verplicht ? " *" : "")</label>
|
||||
|
||||
@switch (veld.Soort)
|
||||
{
|
||||
case KluisVeld.Lang:
|
||||
<textarea id="@VeldId(veld)" rows="3" value="@Waarde(veld)"
|
||||
@onchange="e => Zet(veld, e.Value?.ToString())"></textarea>
|
||||
break;
|
||||
|
||||
case KluisVeld.Datum:
|
||||
<Datumkiezer Waarde="@DatumWaarde(veld)" WaardeChanged="d => ZetDatum(veld, d)"
|
||||
Optioneel="true" VanJaar="@(DateTime.Today.Year - 80)" TotJaar="@(DateTime.Today.Year + 5)"
|
||||
Aflopend="true" />
|
||||
break;
|
||||
|
||||
case KluisVeld.Keuze:
|
||||
<select id="@VeldId(veld)" value="@Waarde(veld)" @onchange="e => Zet(veld, e.Value?.ToString())">
|
||||
<option value="">Kies…</option>
|
||||
@foreach (var optie in veld.Opties)
|
||||
{
|
||||
<option value="@optie" selected="@(optie == Waarde(veld))">@optie</option>
|
||||
}
|
||||
@* Stond er een waarde die niet meer in de lijst staat, dan blijft die zichtbaar. *@
|
||||
@if (HeeftWaarde(veld) && !veld.Opties.Contains(Waarde(veld)))
|
||||
{
|
||||
<option value="@Waarde(veld)" selected="selected">@Waarde(veld)</option>
|
||||
}
|
||||
</select>
|
||||
break;
|
||||
|
||||
default:
|
||||
<input id="@VeldId(veld)" value="@Waarde(veld)" @onchange="e => Zet(veld, e.Value?.ToString())" />
|
||||
break;
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(veld.Hulptekst))
|
||||
{
|
||||
<div class="uitleg">@veld.Hulptekst</div>
|
||||
}
|
||||
}
|
||||
</div>;
|
||||
|
||||
private static string VeldId(KluisVeld veld) => "veld-" + veld.Sleutel;
|
||||
|
||||
/// <summary>
|
||||
/// Muziek en Wensen mogen meerdere regels worden; dan is een vast aantal regels niet
|
||||
/// genoeg en een schuifbalk niet fijn op een telefoon. Deze twee velden groeien daarom
|
||||
|
||||
@@ -156,7 +156,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Tonen.Categorie(item.Category)</div>
|
||||
<div class="zacht">@item.SoortNaam(IngebouwdeSoorten.Naam)</div>
|
||||
</div>
|
||||
<button class="knop rustig klein" disabled="@Bezig" @onclick:stopPropagation="true"
|
||||
@onclick="() => ItemOpenenAsync(item)">Openen</button>
|
||||
@@ -187,7 +187,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Tonen.Categorie(item.Category)</div>
|
||||
<div class="zacht">@item.SoortNaam(IngebouwdeSoorten.Naam)</div>
|
||||
</div>
|
||||
<button class="knop rustig klein" disabled="@Bezig" @onclick="() => ItemOpenenAsync(item)">Openen</button>
|
||||
</div>
|
||||
@@ -205,7 +205,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
||||
<div class="stapel">
|
||||
@foreach (var regel in Regels(Inhoud))
|
||||
@foreach (var regel in Regels(Inhoud).Concat(Inhoud.EigenRegels(GeopendSoort)))
|
||||
{
|
||||
<div class="veld">
|
||||
<label>@regel.Naam</label>
|
||||
@@ -482,6 +482,9 @@ else if (Stand is { IsBevestigd: true })
|
||||
private long? Bekeken, GeopendItem;
|
||||
private IReadOnlyList<KluisItem> Nagelaten = Array.Empty<KluisItem>();
|
||||
private KluisInhoud? Inhoud;
|
||||
|
||||
/// <summary>De soort van het geopende item, voor de labels van de velden van een eigen soort.</summary>
|
||||
private KluisSoort? GeopendSoort;
|
||||
private string? GeopendTitel;
|
||||
private IReadOnlyList<Bijlage> GeopendBijlagen = Array.Empty<Bijlage>();
|
||||
|
||||
@@ -604,6 +607,16 @@ else if (Stand is { IsBevestigd: true })
|
||||
Inhoud = r.Waarde;
|
||||
GeopendTitel = item.Title;
|
||||
|
||||
// De labels van de velden van een soort die de beheerder zelf maakte staan niet in de
|
||||
// app. Ze worden apart opgehaald, ook als die soort niet meer wordt aangeboden: de
|
||||
// namen van de velden zijn geen geheim, de ingevulde waarden blijven versleuteld.
|
||||
GeopendSoort = null;
|
||||
if (item.Category >= 100)
|
||||
{
|
||||
var soort = await Toestand.Client.KluisSoortAsync(item.Category);
|
||||
if (soort.Gelukt) GeopendSoort = soort.Waarde;
|
||||
}
|
||||
|
||||
// Bijlagen zijn hier optioneel: als ophalen mislukt tonen we het item gewoon
|
||||
// zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
|
||||
var bijlagen = await Toestand.Client.BijlagenAsync(item.Id);
|
||||
@@ -616,6 +629,7 @@ else if (Stand is { IsBevestigd: true })
|
||||
{
|
||||
GeopendItem = null;
|
||||
Inhoud = null;
|
||||
GeopendSoort = null;
|
||||
GeopendTitel = null;
|
||||
GeopendBijlagen = Array.Empty<Bijlage>();
|
||||
BijlageDichtdoen();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Application.Models;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.Application.Abstractions;
|
||||
@@ -261,6 +262,31 @@ public interface IAuditLog
|
||||
string? details = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>De soorten kluisitems en hun velden, zoals de beheerder ze instelt.</summary>
|
||||
public interface IVaultCategoryRepository
|
||||
{
|
||||
/// <summary>Alle soorten, met hun velden, op volgorde.</summary>
|
||||
Task<IReadOnlyList<VaultCategoryDefinition>> ListAsync(CancellationToken ct = default);
|
||||
|
||||
Task<VaultCategoryDefinition?> FindAsync(int id, CancellationToken ct = default);
|
||||
|
||||
Task<VaultCategoryField?> FindFieldAsync(long fieldId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Per soort hoeveel kluisitems (niet verwijderd) en hoeveel eigenaren. Alleen aantallen.</summary>
|
||||
Task<IReadOnlyList<SoortGebruik>> GebruikAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>De soorten waar deze persoon minstens één kluisitem (niet verwijderd) van heeft.</summary>
|
||||
Task<IReadOnlySet<int>> InGebruikDoorAsync(long personId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Hoeveel kluisitems (niet verwijderd) er van deze soort zijn.</summary>
|
||||
Task<int> AantalItemsAsync(int categoryId, CancellationToken ct = default);
|
||||
|
||||
void Add(VaultCategoryDefinition soort);
|
||||
void Remove(VaultCategoryDefinition soort);
|
||||
void Add(VaultCategoryField veld);
|
||||
void Remove(VaultCategoryField veld);
|
||||
}
|
||||
|
||||
/// <summary>Versturen van uitnodigingen en meldingen.</summary>
|
||||
/// <summary>
|
||||
/// Adverteerders, hun campagnes en de tellers per uur. Zie docs/Adverteren.md.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace MijnBesluit.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Een soort kluisitem zoals een gebruiker die te zien krijgt: de naam, de uitleg en bij een
|
||||
/// nieuwe soort de velden van het formulier. Dit is ook wat er over de lijn naar de app gaat.
|
||||
/// </summary>
|
||||
public record KluisSoort(int Id, string Naam, string? Uitleg, bool BesprekenMet, bool Ingebouwd,
|
||||
IReadOnlyList<KluisVeld> Velden);
|
||||
|
||||
/// <summary>Eén veld van het formulier van een nieuwe soort.</summary>
|
||||
public record KluisVeld(string Sleutel, string Label, string Soort, IReadOnlyList<string> Opties,
|
||||
string? Hulptekst, bool Verplicht, bool Actief);
|
||||
|
||||
/// <summary>Hoe vaak een soort in gebruik is: aantallen, nooit inhoud of namen.</summary>
|
||||
public record SoortGebruik(int CategoryId, int Items, int Eigenaren);
|
||||
|
||||
/// <summary>Een soort in het beheerscherm, met hoe vaak hij gebruikt wordt.</summary>
|
||||
public record SoortBeheerbeeld(int Id, string Naam, string? Uitleg, bool Actief, int Volgorde,
|
||||
bool BesprekenMet, bool Ingebouwd, int Items, int Eigenaren,
|
||||
IReadOnlyList<VeldBeheerbeeld> Velden)
|
||||
{
|
||||
/// <summary>Verwijderen kan alleen als niemand er een item mee heeft gemaakt en hij niet ingebouwd is.</summary>
|
||||
public bool KanVerwijderen => !Ingebouwd && Items == 0;
|
||||
}
|
||||
|
||||
/// <summary>Een veld in het beheerscherm.</summary>
|
||||
public record VeldBeheerbeeld(long Id, string Sleutel, string Label, string Soort, string? Opties,
|
||||
string? Hulptekst, bool Verplicht, bool Actief, int Volgorde);
|
||||
@@ -35,4 +35,10 @@ public class VaultPayload
|
||||
public string? Music { get; set; }
|
||||
public string? Speakers { get; set; }
|
||||
public string? Wishes { get; set; }
|
||||
|
||||
// Soorten die de beheerder zelf heeft gemaakt (nummer 100 en hoger). Hun velden staan niet als
|
||||
// eigen eigenschap hierboven maar hier, op de vaste sleutel van het veld
|
||||
// (VaultCategoryField.Sleutel). Zo kan er een veld bij zonder dat de code verandert, en blijft
|
||||
// het net zo versleuteld als de rest van de inhoud. Bij de ingebouwde soorten is dit leeg.
|
||||
public Dictionary<string, string>? Velden { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using MijnBesluit.Application.Abstractions;
|
||||
using MijnBesluit.Application.Models;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems. Wat je in de keuzelijst ziet, en bij een door de beheerder gemaakte soort
|
||||
/// ook welke velden het formulier heeft, komt hier vandaan en niet meer uit de code. Zo vraagt een
|
||||
/// nieuwe soort ("Dementie", "Niet reanimeren") geen nieuwe versie van de app.
|
||||
///
|
||||
/// Twee regels bepalen alles wat hier gebeurt:
|
||||
///
|
||||
/// 1. Wat in gebruik is, verdwijnt nooit. Een soort waar al een kluisitem mee is gemaakt kan niet
|
||||
/// worden verwijderd, alleen op niet-actief gezet. Wie er nog nooit een item mee maakte ziet hem
|
||||
/// dan niet meer in zijn keuzelijst; wie dat wel deed, blijft hem zien en kan zijn items
|
||||
/// gewoon bekijken en wijzigen.
|
||||
/// 2. De beheerder ziet aantallen, nooit inhoud. Hoe vaak een soort gebruikt wordt is een tel;
|
||||
/// wie het deed en wat er in staat, blijft versleuteld en onbereikbaar.
|
||||
/// </summary>
|
||||
public class VaultCategoryService
|
||||
{
|
||||
/// <summary>Meer velden dan dit maakt van een formulier een enquête.</summary>
|
||||
public const int MaxVelden = 40;
|
||||
|
||||
/// <summary>Hoe lang een ingevulde waarde in een eigen veld mag zijn.</summary>
|
||||
public const int MaxWaardelengte = 4000;
|
||||
|
||||
private readonly IVaultCategoryRepository _soorten;
|
||||
private readonly IUserRepository _users;
|
||||
private readonly IAuditLog _audit;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public VaultCategoryService(IVaultCategoryRepository soorten, IUserRepository users, IAuditLog audit,
|
||||
IUnitOfWork uow, IClock clock)
|
||||
{
|
||||
_soorten = soorten; _users = users; _audit = audit; _uow = uow; _clock = clock;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ voor gebruikers
|
||||
|
||||
/// <summary>
|
||||
/// De soorten die deze persoon te zien krijgt: alle actieve, plus de niet-actieve waar hij al
|
||||
/// een item van heeft. Op volgorde van de beheerder.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<KluisSoort>> ZichtbaarVoorAsync(long personId, CancellationToken ct = default)
|
||||
{
|
||||
var alle = await _soorten.ListAsync(ct);
|
||||
var gebruikt = await _soorten.InGebruikDoorAsync(personId, ct);
|
||||
|
||||
return alle.Where(s => s.Actief || gebruikt.Contains(s.Id))
|
||||
.Select(NaarSoort)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Alle namen op nummer, voor onder een kluisitem. Ook van soorten die niet meer actief zijn.</summary>
|
||||
public async Task<IReadOnlyDictionary<int, string>> NamenAsync(CancellationToken ct = default) =>
|
||||
(await _soorten.ListAsync(ct)).ToDictionary(s => s.Id, s => s.Naam);
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort op nummer, ook als hij niet actief is of de vrager hem nooit gebruikte. Nodig om
|
||||
/// het item van een ander te kunnen lezen (een nalatenschap, iets dat met je is gedeeld): de
|
||||
/// namen van de velden zijn geen geheim, de ingevulde waarden wel, en die blijven versleuteld.
|
||||
/// </summary>
|
||||
public async Task<KluisSoort?> SoortAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var soort = await _soorten.FindAsync(id, ct);
|
||||
return soort is null ? null : NaarSoort(soort);
|
||||
}
|
||||
|
||||
/// <summary>De soort met dit nummer, met velden. Null als hij niet bestaat.</summary>
|
||||
public Task<VaultCategoryDefinition?> VindAsync(int id, CancellationToken ct = default) =>
|
||||
_soorten.FindAsync(id, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Mag deze persoon een nieuw kluisitem in deze soort maken? Alleen als de soort bestaat en
|
||||
/// actief is, of als hij er al een item van heeft - dan is het voor hem nog gewoon een soort
|
||||
/// die hij kent.
|
||||
/// </summary>
|
||||
public async Task<Result<VaultCategoryDefinition>> MagGebruikenAsync(long personId, int categoryId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var soort = await _soorten.FindAsync(categoryId, ct);
|
||||
if (soort is null) return Result<VaultCategoryDefinition>.Fail("Deze soort bestaat niet.");
|
||||
|
||||
if (!soort.Actief && !(await _soorten.InGebruikDoorAsync(personId, ct)).Contains(soort.Id))
|
||||
return Result<VaultCategoryDefinition>.Fail($"{soort.Naam} wordt niet meer aangeboden.");
|
||||
|
||||
return Result<VaultCategoryDefinition>.Ok(soort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controleert wat er in de eigen velden van een door de beheerder gemaakte soort is ingevuld
|
||||
/// en geeft de schone versie terug: alleen bekende velden, bijgesneden, met de verplichte
|
||||
/// velden ingevuld en datums die kloppen. Op de server en niet alleen in het scherm, want
|
||||
/// een oudere of andere app stuurt wat hij wil.
|
||||
/// </summary>
|
||||
public static Result<Dictionary<string, string>> ControleerVelden(
|
||||
VaultCategoryDefinition soort, IReadOnlyDictionary<string, string?>? ingevuld)
|
||||
{
|
||||
var schoon = new Dictionary<string, string>();
|
||||
|
||||
foreach (var veld in soort.Velden.Where(v => v.Soort != VeldSoort.Uitleg))
|
||||
{
|
||||
ingevuld ??= new Dictionary<string, string?>();
|
||||
var waarde = ingevuld.TryGetValue(veld.Sleutel, out var w) ? w?.Trim() : null;
|
||||
|
||||
if (string.IsNullOrEmpty(waarde))
|
||||
{
|
||||
if (veld.Verplicht && veld.Actief)
|
||||
return Result<Dictionary<string, string>>.Fail($"Vul in: {veld.Label}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waarde.Length > MaxWaardelengte)
|
||||
return Result<Dictionary<string, string>>.Fail($"{veld.Label} is te lang.");
|
||||
|
||||
if (veld.Soort == VeldSoort.Datum &&
|
||||
!DateOnly.TryParseExact(waarde, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out _))
|
||||
return Result<Dictionary<string, string>>.Fail($"{veld.Label}: dat is geen geldige datum.");
|
||||
|
||||
schoon[veld.Sleutel] = waarde;
|
||||
}
|
||||
|
||||
return Result<Dictionary<string, string>>.Ok(schoon);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ voor de beheerder
|
||||
|
||||
/// <summary>Alle soorten met hoe vaak ze gebruikt worden. Alleen voor beheerders.</summary>
|
||||
public async Task<Result<IReadOnlyList<SoortBeheerbeeld>>> OverzichtAsync(long currentUserId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct))
|
||||
return Result<IReadOnlyList<SoortBeheerbeeld>>.Fail("Je bent geen beheerder.");
|
||||
|
||||
var alle = await _soorten.ListAsync(ct);
|
||||
var gebruik = (await _soorten.GebruikAsync(ct)).ToDictionary(g => g.CategoryId);
|
||||
|
||||
var lijst = alle.Select(s =>
|
||||
{
|
||||
gebruik.TryGetValue(s.Id, out var g);
|
||||
return new SoortBeheerbeeld(
|
||||
s.Id, s.Naam, s.Uitleg, s.Actief, s.Volgorde, s.BesprekenMet, s.Ingebouwd,
|
||||
g?.Items ?? 0, g?.Eigenaren ?? 0,
|
||||
s.Velden.OrderBy(v => v.Volgorde).ThenBy(v => v.Id)
|
||||
.Select(v => new VeldBeheerbeeld(v.Id, v.Sleutel, v.Label, v.Soort, v.Opties,
|
||||
v.Hulptekst, v.Verplicht, v.Actief, v.Volgorde))
|
||||
.ToList());
|
||||
}).ToList();
|
||||
|
||||
return Result<IReadOnlyList<SoortBeheerbeeld>>.Ok(lijst);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maakt een nieuwe soort. Hij begint actief, onderaan de lijst. De naam is leesbaar voor de
|
||||
/// server (het item zelf blijft versleuteld), dus een naam die iets over iemands gezondheid
|
||||
/// zegt, zegt dat ook tegen de server.
|
||||
/// </summary>
|
||||
public async Task<Result<int>> MaakAsync(long currentUserId, string naam, string? uitleg, bool besprekenMet,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result<int>.Fail("Je bent geen beheerder.");
|
||||
|
||||
var fout = await ControleerNaamAsync(null, naam, uitleg, ct);
|
||||
if (fout is not null) return Result<int>.Fail(fout);
|
||||
|
||||
var alle = await _soorten.ListAsync(ct);
|
||||
var nu = _clock.UtcNow;
|
||||
var soort = new VaultCategoryDefinition
|
||||
{
|
||||
// Nummers van eigen soorten beginnen bij 100; de database kiest het volgende vrije.
|
||||
Naam = naam.Trim(),
|
||||
Uitleg = Schoon(uitleg),
|
||||
Actief = true,
|
||||
Volgorde = (alle.Count == 0 ? 0 : alle.Max(s => s.Volgorde)) + 10,
|
||||
BesprekenMet = besprekenMet,
|
||||
Ingebouwd = false,
|
||||
AangemaaktOp = nu,
|
||||
GewijzigdOp = nu
|
||||
};
|
||||
|
||||
_soorten.Add(soort);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition),
|
||||
soort.Id, $"gemaakt: {soort.Naam}", ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result<int>.Ok(soort.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wijzigt een soort: de naam, de uitleg, of hij nog wordt aangeboden, waar hij in de lijst
|
||||
/// staat en of er "Bespreken met" bij zit. Dit kan ook bij de ingebouwde soorten.
|
||||
/// </summary>
|
||||
public async Task<Result> WijzigAsync(long currentUserId, int id, string naam, string? uitleg, bool actief,
|
||||
int volgorde, bool besprekenMet, CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var soort = await _soorten.FindAsync(id, ct);
|
||||
if (soort is null) return Result.Fail("Deze soort bestaat niet.");
|
||||
|
||||
var fout = await ControleerNaamAsync(id, naam, uitleg, ct);
|
||||
if (fout is not null) return Result.Fail(fout);
|
||||
|
||||
var wijzigingen = new List<string>();
|
||||
if (soort.Naam != naam.Trim()) wijzigingen.Add($"naam {soort.Naam} -> {naam.Trim()}");
|
||||
if (soort.Actief != actief) wijzigingen.Add(actief ? "actief" : "niet-actief");
|
||||
if (soort.BesprekenMet != besprekenMet) wijzigingen.Add(besprekenMet ? "bespreken met aan" : "bespreken met uit");
|
||||
|
||||
soort.Naam = naam.Trim();
|
||||
soort.Uitleg = Schoon(uitleg);
|
||||
soort.Actief = actief;
|
||||
soort.Volgorde = volgorde;
|
||||
soort.BesprekenMet = besprekenMet;
|
||||
soort.GewijzigdOp = _clock.UtcNow;
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition), id,
|
||||
wijzigingen.Count == 0 ? "gewijzigd" : string.Join(", ", wijzigingen), ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verwijdert een soort - maar alleen een door de beheerder gemaakte waar nog nooit een
|
||||
/// kluisitem mee is gemaakt. Is hij in gebruik, dan blijft alleen "niet-actief" over, zodat
|
||||
/// wie hem gebruikt hem houdt.
|
||||
/// </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 soort = await _soorten.FindAsync(id, ct);
|
||||
if (soort is null) return Result.Fail("Deze soort bestaat niet.");
|
||||
if (soort.Ingebouwd) return Result.Fail("Een ingebouwde soort kun je niet verwijderen, alleen niet-actief maken.");
|
||||
|
||||
var aantal = await _soorten.AantalItemsAsync(id, ct);
|
||||
if (aantal > 0)
|
||||
return Result.Fail($"{soort.Naam} is in gebruik ({aantal} " +
|
||||
(aantal == 1 ? "item" : "items") + "). Zet hem op niet-actief; dan zien alleen de mensen die hem gebruiken hem nog.");
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition), id,
|
||||
$"verwijderd: {soort.Naam}", ct);
|
||||
_soorten.Remove(soort);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>Voegt een veld toe aan het formulier van een door de beheerder gemaakte soort.</summary>
|
||||
public async Task<Result> VeldToevoegenAsync(long currentUserId, int categoryId, string label, string soortVeld,
|
||||
string? opties, string? hulptekst, bool verplicht,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var soort = await _soorten.FindAsync(categoryId, ct);
|
||||
if (soort is null) return Result.Fail("Deze soort bestaat niet.");
|
||||
if (soort.Ingebouwd) return Result.Fail("De velden van een ingebouwde soort zitten in de app en zijn hier niet te wijzigen.");
|
||||
if (soort.Velden.Count >= MaxVelden) return Result.Fail($"Meer dan {MaxVelden} velden is te veel voor één formulier.");
|
||||
|
||||
var fout = ControleerVeld(label, soortVeld, opties, hulptekst, verplicht);
|
||||
if (fout is not null) return Result.Fail(fout);
|
||||
|
||||
var veld = new VaultCategoryField
|
||||
{
|
||||
CategoryId = categoryId,
|
||||
Sleutel = NieuweSleutel(label, soort.Velden.Select(v => v.Sleutel)),
|
||||
Label = label.Trim(),
|
||||
Soort = soortVeld,
|
||||
Opties = soortVeld == VeldSoort.Keuze ? SchoneOpties(opties) : null,
|
||||
Hulptekst = Schoon(hulptekst),
|
||||
Verplicht = verplicht && soortVeld != VeldSoort.Uitleg,
|
||||
Actief = true,
|
||||
Volgorde = (soort.Velden.Count == 0 ? 0 : soort.Velden.Max(v => v.Volgorde)) + 10
|
||||
};
|
||||
|
||||
_soorten.Add(veld);
|
||||
soort.GewijzigdOp = _clock.UtcNow;
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition), categoryId,
|
||||
$"veld toegevoegd: {veld.Label}", ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wijzigt een veld. De sleutel en de soort veld staan vast, want daaraan hangt wat mensen al
|
||||
/// hebben ingevuld; het label, de opties, de hulptekst, verplicht, actief en de plek mogen wel.
|
||||
/// </summary>
|
||||
public async Task<Result> VeldWijzigenAsync(long currentUserId, long veldId, string label, string? opties,
|
||||
string? hulptekst, bool verplicht, bool actief, int volgorde,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var veld = await _soorten.FindFieldAsync(veldId, ct);
|
||||
if (veld is null) return Result.Fail("Dit veld bestaat niet.");
|
||||
|
||||
var fout = ControleerVeld(label, veld.Soort, opties, hulptekst, verplicht);
|
||||
if (fout is not null) return Result.Fail(fout);
|
||||
|
||||
veld.Label = label.Trim();
|
||||
veld.Opties = veld.Soort == VeldSoort.Keuze ? SchoneOpties(opties) : null;
|
||||
veld.Hulptekst = Schoon(hulptekst);
|
||||
veld.Verplicht = verplicht && veld.Soort != VeldSoort.Uitleg;
|
||||
veld.Actief = actief;
|
||||
veld.Volgorde = volgorde;
|
||||
|
||||
var soort = await _soorten.FindAsync(veld.CategoryId, ct);
|
||||
if (soort is not null) soort.GewijzigdOp = _clock.UtcNow;
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition),
|
||||
veld.CategoryId, $"veld gewijzigd: {veld.Label}", ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Haalt een veld weg. Alleen zolang er geen kluisitem van deze soort bestaat: anders staan
|
||||
/// er ingevulde gegevens onder en is "niet-actief" de weg - die waarden blijven dan gewoon
|
||||
/// bewaard en zichtbaar bij de items die ze hebben.
|
||||
/// </summary>
|
||||
public async Task<Result> VeldVerwijderenAsync(long currentUserId, long veldId, CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsBeheerderAsync(currentUserId, ct)) return Result.Fail("Je bent geen beheerder.");
|
||||
|
||||
var veld = await _soorten.FindFieldAsync(veldId, ct);
|
||||
if (veld is null) return Result.Fail("Dit veld bestaat niet.");
|
||||
|
||||
if (await _soorten.AantalItemsAsync(veld.CategoryId, ct) > 0)
|
||||
return Result.Fail("Er zijn al items van deze soort; zet het veld op niet-actief in plaats van het te verwijderen. Wat er is ingevuld blijft dan bewaard.");
|
||||
|
||||
await _audit.WriteAsync(AuditAction.KluisSoortBeheerd, currentUserId, nameof(VaultCategoryDefinition),
|
||||
veld.CategoryId, $"veld verwijderd: {veld.Label}", ct);
|
||||
_soorten.Remove(veld);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ hulp
|
||||
|
||||
private async Task<bool> IsBeheerderAsync(long userId, CancellationToken ct) =>
|
||||
(await _users.FindByIdAsync(userId, ct))?.IsBeheerder == true;
|
||||
|
||||
private async Task<string?> ControleerNaamAsync(int? eigenId, string? naam, string? uitleg, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(naam)) return "Geef de soort een naam.";
|
||||
if (naam.Trim().Length > 80) return "Een naam van meer dan 80 tekens is te lang.";
|
||||
if (uitleg is { Length: > 500 }) return "De uitleg mag niet langer zijn dan 500 tekens.";
|
||||
|
||||
var dubbel = (await _soorten.ListAsync(ct))
|
||||
.Any(s => s.Id != eigenId && string.Equals(s.Naam, naam.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
return dubbel ? "Er bestaat al een soort met deze naam." : null;
|
||||
}
|
||||
|
||||
private static string? ControleerVeld(string? label, string? soortVeld, string? opties, string? hulptekst, bool verplicht)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(label)) return "Geef het veld een naam.";
|
||||
if (label.Trim().Length > 100) return "De naam van een veld mag niet langer zijn dan 100 tekens.";
|
||||
if (!VeldSoort.Bekend(soortVeld)) return "Dit soort veld kennen we niet.";
|
||||
if (hulptekst is { Length: > 300 }) return "De hulptekst mag niet langer zijn dan 300 tekens.";
|
||||
|
||||
if (soortVeld == VeldSoort.Keuze)
|
||||
{
|
||||
var lijst = (SchoneOpties(opties) ?? "").Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lijst.Length < 2) return "Een keuzelijst heeft minstens twee opties nodig, één per regel.";
|
||||
if ((SchoneOpties(opties) ?? "").Length > 1000) return "De opties zijn samen te lang.";
|
||||
}
|
||||
|
||||
if (soortVeld == VeldSoort.Uitleg && string.IsNullOrWhiteSpace(hulptekst))
|
||||
return "Een uitlegtekst heeft een tekst nodig; vul die in bij de hulptekst.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Opties één per regel, zonder lege regels en dubbelen.</summary>
|
||||
private static string? SchoneOpties(string? opties) =>
|
||||
opties is null
|
||||
? null
|
||||
: string.Join('\n', opties.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
private static string? Schoon(string? tekst) => string.IsNullOrWhiteSpace(tekst) ? null : tekst.Trim();
|
||||
|
||||
private static readonly Dictionary<char, string> Accenten = BouwAccenten();
|
||||
|
||||
private static Dictionary<char, string> BouwAccenten()
|
||||
{
|
||||
var d = new Dictionary<char, string>();
|
||||
void Voeg(string letters, string door) { foreach (var l in letters) d[l] = door; }
|
||||
Voeg("àáâãäåāăą", "a"); Voeg("çćĉċč", "c"); Voeg("ďđ", "d"); Voeg("èéêëēĕėęě", "e");
|
||||
Voeg("ĝğġģ", "g"); Voeg("ĥħ", "h"); Voeg("ìíîïĩīĭįı", "i"); Voeg("ĵ", "j"); Voeg("ķ", "k");
|
||||
Voeg("ĺļľŀł", "l"); Voeg("ñńņňʼn", "n"); Voeg("òóôõöøōŏő", "o"); Voeg("ŕŗř", "r");
|
||||
Voeg("śŝşš", "s"); Voeg("ţťŧ", "t"); Voeg("ùúûüũūŭůűų", "u"); Voeg("ŵ", "w");
|
||||
Voeg("ýÿŷ", "y"); Voeg("źżž", "z"); d['ß'] = "ss"; d['æ'] = "ae"; d['œ'] = "oe"; d['þ'] = "th";
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Een vaste, korte naam voor een veld, afgeleid van het label: "Wil je gereanimeerd worden?"
|
||||
/// wordt "wil_je_gereanimeerd_worden". Is die al in gebruik in deze soort, dan komt er een
|
||||
/// nummer achter.
|
||||
/// </summary>
|
||||
public static string NieuweSleutel(string label, IEnumerable<string> bestaand)
|
||||
{
|
||||
var bouw = new StringBuilder();
|
||||
foreach (var teken in label.ToLowerInvariant())
|
||||
{
|
||||
// Accenten zelf wegwerken in plaats van via string.Normalize: dat werkt niet overal
|
||||
// hetzelfde zodra InvariantGlobalization aan staat (zoals hier), en de sleutel mag niet
|
||||
// per machine verschillen.
|
||||
var ascii = Accenten.TryGetValue(teken, out var zonder) ? zonder : teken.ToString();
|
||||
foreach (var t in ascii)
|
||||
{
|
||||
if (char.IsAsciiLetterOrDigit(t)) bouw.Append(t);
|
||||
else if (bouw.Length > 0 && bouw[^1] != '_') bouw.Append('_');
|
||||
}
|
||||
}
|
||||
|
||||
var basis = bouw.ToString().Trim('_');
|
||||
if (basis.Length == 0) basis = "veld";
|
||||
if (basis.Length > 30) basis = basis[..30].TrimEnd('_');
|
||||
|
||||
var gebruikt = new HashSet<string>(bestaand, StringComparer.OrdinalIgnoreCase);
|
||||
var sleutel = basis;
|
||||
for (var n = 2; gebruikt.Contains(sleutel); n++) sleutel = $"{basis}_{n}";
|
||||
return sleutel;
|
||||
}
|
||||
|
||||
private static KluisSoort NaarSoort(VaultCategoryDefinition s) =>
|
||||
new(s.Id, s.Naam, s.Uitleg, s.BesprekenMet, s.Ingebouwd,
|
||||
s.Velden.OrderBy(v => v.Volgorde).ThenBy(v => v.Id)
|
||||
.Select(v => new KluisVeld(v.Sleutel, v.Label, v.Soort, v.OptieLijst, v.Hulptekst, v.Verplicht, v.Actief))
|
||||
.ToList());
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class VaultService
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IClock _clock;
|
||||
private readonly IAfbeeldingVerkleiner _verkleiner;
|
||||
private readonly VaultCategoryService? _soorten;
|
||||
|
||||
/// <summary>De langste zijde van de kleine versie die bij een foto wordt bewaard.</summary>
|
||||
public const int ThumbnailZijde = 160;
|
||||
@@ -46,12 +47,14 @@ public class VaultService
|
||||
public VaultService(IVaultRepository vault, IPersonRepository people, IMessageRepository messages,
|
||||
IUserRepository users, IMasterKeyProtector keys, IFileStore files, IAuditLog audit,
|
||||
IUnitOfWork uow, IClock clock, IAfbeeldingVerkleiner? verkleiner = null,
|
||||
IFamilyRepository? families = null, ILegacyRepository? legacy = null)
|
||||
IFamilyRepository? families = null, ILegacyRepository? legacy = null,
|
||||
VaultCategoryService? soorten = null)
|
||||
{
|
||||
_vault = vault; _people = people; _messages = messages; _users = users; _keys = keys;
|
||||
_files = files; _audit = audit; _uow = uow; _clock = clock; _families = families;
|
||||
_verkleiner = verkleiner ?? new GeenAfbeeldingVerkleiner();
|
||||
_legacy = legacy;
|
||||
_soorten = soorten;
|
||||
}
|
||||
|
||||
/// <summary>Titels en categorieën van de kluis. De inhoud blijft versleuteld tot hij echt geopend wordt.</summary>
|
||||
@@ -71,6 +74,10 @@ public class VaultService
|
||||
if (release == ReleasePolicy.OpDatum && releaseAt is null)
|
||||
return Result<VaultItem>.Fail("Kies een datum waarop dit item vrijkomt.");
|
||||
|
||||
var soortControle = await ControleerSoortAsync(user, category, payload, nieuw: true, aanbodControleren: true, ct);
|
||||
if (!soortControle.Succeeded) return Result<VaultItem>.Fail(soortControle.Error!);
|
||||
var kanBespreken = await KanBesprekenMetAsync(category, ct);
|
||||
|
||||
var now = _clock.UtcNow;
|
||||
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
||||
var contentKey = EnvelopeCrypto.NewKey();
|
||||
@@ -89,7 +96,7 @@ public class VaultService
|
||||
WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey),
|
||||
ReleasePolicy = release,
|
||||
ReleaseAt = releaseAt,
|
||||
SharedManagerPersonId = KanBesprekenMet(category) ? sharedManagerPersonId : null,
|
||||
SharedManagerPersonId = kanBespreken ? sharedManagerPersonId : null,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
@@ -132,6 +139,12 @@ public class VaultService
|
||||
if (release == ReleasePolicy.OpDatum && releaseAt is null)
|
||||
return Result.Fail("Kies een datum waarop dit item vrijkomt.");
|
||||
|
||||
// Blijft het item in dezelfde soort, dan is niet-actief geen bezwaar: het is zijn eigen item.
|
||||
var soortControle = await ControleerSoortAsync(user, category, payload, nieuw: false,
|
||||
aanbodControleren: item.Category != category, ct);
|
||||
if (!soortControle.Succeeded) return Result.Fail(soortControle.Error!);
|
||||
var kanBespreken = await KanBesprekenMetAsync(category, ct);
|
||||
|
||||
var now = _clock.UtcNow;
|
||||
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
||||
var contentKey = EnvelopeCrypto.NewKey();
|
||||
@@ -147,7 +160,7 @@ public class VaultService
|
||||
item.WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey);
|
||||
item.ReleasePolicy = release;
|
||||
item.ReleaseAt = releaseAt;
|
||||
item.SharedManagerPersonId = KanBesprekenMet(category) ? sharedManagerPersonId : null;
|
||||
item.SharedManagerPersonId = kanBespreken ? sharedManagerPersonId : null;
|
||||
item.UpdatedAt = now;
|
||||
|
||||
// Eerder verleende toegang was op de oude sleutel; die is nu waardeloos. Wie het item
|
||||
@@ -320,6 +333,66 @@ public class VaultService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bij welke soorten kluisitems een beheerder te kiezen is om vooraf mee te bespreken. Dat is
|
||||
/// een instelling per soort die de beheerder op de website zet; voorheen stond hij vast op
|
||||
/// uitvaartwens en donorcodicil, en dat zijn ook de startwaarden. Zonder soortenlijst (een
|
||||
/// oude opzet, of een test) geldt nog die oude regel.
|
||||
/// </summary>
|
||||
private async Task<bool> KanBesprekenMetAsync(VaultCategory category, CancellationToken ct)
|
||||
{
|
||||
if (_soorten is null)
|
||||
return category is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil;
|
||||
|
||||
return (await _soorten.VindAsync((int)category, ct))?.BesprekenMet == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kijkt of dit soort item gemaakt of gewijzigd mag worden en maakt de eigen velden schoon.
|
||||
///
|
||||
/// <paramref name="aanbodControleren"/>: wordt de soort nog aangeboden aan deze persoon? Dat
|
||||
/// geldt bij een nieuw item, en bij een item dat van soort wisselt. Een bestaand item dat in
|
||||
/// zijn eigen soort blijft, hoeft dat niet: wie een soort al gebruikt, houdt hem.
|
||||
///
|
||||
/// Een door de beheerder gemaakte soort bewaart zijn velden in <see cref="VaultPayload.Velden"/>.
|
||||
/// Komt er bij het wijzigen niets mee, dan is dat een app die de soort niet kent - die zou de
|
||||
/// ingevulde gegevens overschrijven met niets, dus dat weigeren we.
|
||||
/// </summary>
|
||||
private async Task<Result> ControleerSoortAsync(User user, VaultCategory category, VaultPayload payload,
|
||||
bool nieuw, bool aanbodControleren, CancellationToken ct)
|
||||
{
|
||||
if (_soorten is null) return Result.Ok();
|
||||
|
||||
VaultCategoryDefinition? soort;
|
||||
if (aanbodControleren)
|
||||
{
|
||||
var mag = await _soorten.MagGebruikenAsync(user.PersonId, (int)category, ct);
|
||||
if (!mag.Succeeded) return Result.Fail(mag.Error!);
|
||||
soort = mag.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
soort = await _soorten.VindAsync((int)category, ct);
|
||||
if (soort is null) return Result.Ok(); // een nummer dat nergens meer bij hoort: niet in de weg zitten
|
||||
}
|
||||
|
||||
if (soort!.Ingebouwd)
|
||||
{
|
||||
payload.Velden = null;
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
if (!nieuw && payload.Velden is null)
|
||||
return Result.Fail("Dit item kan alleen met een bijgewerkte versie van de app worden gewijzigd. Werk de app bij en probeer het opnieuw.");
|
||||
|
||||
var schoon = VaultCategoryService.ControleerVelden(
|
||||
soort, payload.Velden?.ToDictionary(v => v.Key, v => (string?)v.Value));
|
||||
if (!schoon.Succeeded) return Result.Fail(schoon.Error!);
|
||||
|
||||
payload.Velden = schoon.Value;
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Geeft de gekozen nalatenschapsbeheerder van een Uitvaartwens meteen leestoegang tot dit
|
||||
/// item, zodat het al vóór een overlijden te bespreken is. Alleen als de gekozen persoon
|
||||
@@ -327,14 +400,6 @@ public class VaultService
|
||||
/// is er geen datasleutel om de contentsleutel opnieuw mee in te pakken). Ontbreekt een van
|
||||
/// beide, dan wordt de keuze stil overgeslagen; het item zelf blijft wel bewaard staan.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Bij welke soorten kluisitems een beheerder te kiezen is om vooraf mee te bespreken. Op dit
|
||||
/// moment een uitvaartwens en een donorcodicil - allebei dingen waar je iemand graag vooraf
|
||||
/// bij betrekt, in plaats van dat diegene het pas na een overlijden voor het eerst ziet.
|
||||
/// </summary>
|
||||
private static bool KanBesprekenMet(VaultCategory category) =>
|
||||
category is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil;
|
||||
|
||||
private async Task DeelMetBeheerderAsync(VaultItem item, User owner, long beheerderPersonId, CancellationToken ct)
|
||||
{
|
||||
if (_legacy is not null)
|
||||
|
||||
@@ -333,6 +333,21 @@ public class MijnBesluitClient : IAsyncDisposable
|
||||
public Task<Uitkomst<IReadOnlyList<KluisItem>>> KluisAsync(int? categorie = null, CancellationToken ct = default) =>
|
||||
StuurAsync<IReadOnlyList<KluisItem>>(SocketActions.VaultList, new { category = categorie }, ct);
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems die ik te zien krijg. Faalt dit of komt er niets terug (een oudere
|
||||
/// server), dan hoort het scherm op zijn ingebouwde lijst terug te vallen - zie
|
||||
/// <see cref="IngebouwdeSoorten"/>.
|
||||
/// </summary>
|
||||
public Task<Uitkomst<IReadOnlyList<KluisSoort>>> KluisSoortenAsync(CancellationToken ct = default) =>
|
||||
StuurAsync<IReadOnlyList<KluisSoort>>(SocketActions.VaultCategories, null, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort op nummer, met zijn velden - ook als die soort niet meer wordt aangeboden. Voor het
|
||||
/// lezen van het item van een ander, waar je eigen lijst de soort niet hoeft te kennen.
|
||||
/// </summary>
|
||||
public Task<Uitkomst<KluisSoort>> KluisSoortAsync(int soortId, CancellationToken ct = default) =>
|
||||
StuurAsync<KluisSoort>(SocketActions.VaultCategory, new { id = soortId }, ct);
|
||||
|
||||
public Task<Uitkomst<KluisInhoud>> KluisOpenenAsync(long itemId, CancellationToken ct = default) =>
|
||||
StuurAsync<KluisInhoud>(SocketActions.VaultOpen, new { id = itemId }, ct);
|
||||
|
||||
|
||||
@@ -246,7 +246,58 @@ public record KluisItem(
|
||||
int ReleasePolicy,
|
||||
DateTime? ReleaseAt,
|
||||
DateTime UpdatedAt,
|
||||
long? SharedManagerPersonId = null);
|
||||
long? SharedManagerPersonId = null,
|
||||
string? CategoryName = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Hoe de soort heet. De server stuurt de naam mee, zodat een soort die de beheerder zelf
|
||||
/// maakte ook in een oudere lijst een naam heeft; ontbreekt hij, dan geldt de ingebouwde naam
|
||||
/// van het nummer.
|
||||
/// </summary>
|
||||
public string SoortNaam(Func<int, string> ingebouwd) =>
|
||||
string.IsNullOrWhiteSpace(CategoryName) ? ingebouwd(Category) : CategoryName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Een soort kluisitem zoals de server hem aanbiedt. Een ingebouwde soort (nummer 1 tot en met 11
|
||||
/// en 99) heeft zijn invulscherm in de app en geen <see cref="Velden"/>; een soort die de
|
||||
/// beheerder maakte heeft een nummer vanaf 100 en beschrijft zijn formulier hier.
|
||||
/// </summary>
|
||||
public record KluisSoort(int Id, string Naam, string? Uitleg, bool BesprekenMet, bool Ingebouwd,
|
||||
IReadOnlyList<KluisVeld> Velden);
|
||||
|
||||
/// <summary>Eén veld van het formulier van een soort die de beheerder maakte.</summary>
|
||||
public record KluisVeld(string Sleutel, string Label, string Soort, IReadOnlyList<string> Opties,
|
||||
string? Hulptekst, bool Verplicht, bool Actief)
|
||||
{
|
||||
public const string Tekst = "tekst";
|
||||
public const string Lang = "lang";
|
||||
public const string Datum = "datum";
|
||||
public const string Keuze = "keuze";
|
||||
public const string Uitleg = "uitleg";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// volgorde als de startrijen in db/21_kluissoorten.sql; een controle in de zelftest bewaakt dat
|
||||
/// ze gelijk blijven.
|
||||
/// </summary>
|
||||
public static class IngebouwdeSoorten
|
||||
{
|
||||
private static KluisSoort S(int id, string naam, bool bespreken = false) =>
|
||||
new(id, naam, null, bespreken, true, Array.Empty<KluisVeld>());
|
||||
|
||||
public static readonly IReadOnlyList<KluisSoort> Lijst = new[]
|
||||
{
|
||||
S(3, "Wachtwoord"), S(8, "Sociale media"), S(4, "Apparaat"), S(5, "Pincode"), S(2, "Polis"),
|
||||
S(1, "Uitvaartwens", true), S(9, "Donorcodicil", true), S(10, "Rijbewijs"), S(11, "Paspoort"),
|
||||
S(6, "Document"), S(7, "Notitie"), S(99, "Overig")
|
||||
};
|
||||
|
||||
/// <summary>De naam bij een nummer; "Overig" voor wat we niet kennen.</summary>
|
||||
public static string Naam(int id) => Lijst.FirstOrDefault(s => s.Id == id)?.Naam ?? "Overig";
|
||||
}
|
||||
|
||||
/// <summary>De ontsleutelde inhoud van een kluisitem. Per soort is maar een deel gevuld.</summary>
|
||||
public class KluisInhoud
|
||||
@@ -273,6 +324,34 @@ public class KluisInhoud
|
||||
public string? Music { get; set; }
|
||||
public string? Speakers { get; set; }
|
||||
public string? Wishes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// De velden van een soort die de beheerder zelf maakte, op de vaste sleutel van het veld.
|
||||
/// Bij de ingebouwde soorten blijft dit leeg.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Velden { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// De ingevulde velden van een soort die de beheerder maakte, met hun label, in de volgorde van
|
||||
/// het formulier. Alleen wat is ingevuld; datums staan er leesbaar, zoals "12 oktober 2026".
|
||||
/// Een veld dat niet meer in de soort staat, wordt niet getoond.
|
||||
/// </summary>
|
||||
public IEnumerable<(string Naam, string Waarde)> EigenRegels(KluisSoort? soort)
|
||||
{
|
||||
if (soort is null) yield break;
|
||||
|
||||
foreach (var veld in soort.Velden.Where(v => v.Soort != KluisVeld.Uitleg))
|
||||
{
|
||||
if (!Velden.TryGetValue(veld.Sleutel, out var waarde) || string.IsNullOrWhiteSpace(waarde)) continue;
|
||||
|
||||
if (veld.Soort == KluisVeld.Datum &&
|
||||
DateOnly.TryParseExact(waarde, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None, out var dag))
|
||||
waarde = $"{dag.Day} {Datums.Maanden[dag.Month - 1]} {dag.Year}";
|
||||
|
||||
yield return (veld.Label, waarde);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Een bijlage bij een kluisitem, zonder de inhoud zelf.</summary>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
namespace MijnBesluit.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort kluisitem zoals de beheerder die instelt: "Wachtwoord", "Polis", of een nieuwe als
|
||||
/// "Niet reanimeren". Het nummer is hetzelfde nummer als <c>VaultItem.Category</c>; de elf
|
||||
/// ingebouwde soorten en Overig (99) houden hun oude nummer, nieuwe soorten beginnen bij 100.
|
||||
///
|
||||
/// Een ingebouwde soort heeft zijn invulscherm in de app zelf en dus geen <see cref="Velden"/>.
|
||||
/// Een nieuwe soort beschrijft zijn velden hier, en de app tekent het formulier daarnaar - zodat
|
||||
/// een nieuwe soort geen nieuwe versie van de app vraagt.
|
||||
/// </summary>
|
||||
public class VaultCategoryDefinition
|
||||
{
|
||||
/// <summary>Vanaf dit nummer zijn soorten door de beheerder gemaakt.</summary>
|
||||
public const int EersteEigenNummer = 100;
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Naam { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Korte uitleg die in de app onder de keuzelijst staat. Mag leeg.</summary>
|
||||
public string? Uitleg { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wordt de soort nog aangeboden? Niet-actief verdwijnt uit de keuzelijst van iedereen die er
|
||||
/// nog nooit een item van maakte; wie dat wel deed, blijft de soort zien.
|
||||
/// </summary>
|
||||
public bool Actief { get; set; } = true;
|
||||
|
||||
public int Volgorde { get; set; } = 1000;
|
||||
|
||||
/// <summary>Mag de eigenaar bij dit soort een beheerder kiezen om het vooraf mee te bespreken?</summary>
|
||||
public bool BesprekenMet { get; set; }
|
||||
|
||||
/// <summary>Invulscherm zit in de app; velden zijn hier niet te wijzigen en de soort is niet te verwijderen.</summary>
|
||||
public bool Ingebouwd { get; set; }
|
||||
|
||||
public DateTime AangemaaktOp { get; set; } = DateTime.UtcNow;
|
||||
public DateTime GewijzigdOp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public List<VaultCategoryField> Velden { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Een veld in het formulier van een door de beheerder gemaakte soort.</summary>
|
||||
public class VaultCategoryField
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// De vaste naam waaronder de waarde in de versleutelde inhoud staat. Wordt bij het maken
|
||||
/// van het veld uit het label afgeleid en wijzigt daarna nooit meer: een label hernoemen mag,
|
||||
/// maar mag geen ingevulde gegevens losmaken van hun veld.
|
||||
/// </summary>
|
||||
public string Sleutel { get; set; } = string.Empty;
|
||||
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Een van <see cref="VeldSoort"/>. Verandert na het maken niet meer.</summary>
|
||||
public string Soort { get; set; } = VeldSoort.Tekst;
|
||||
|
||||
/// <summary>Bij <see cref="VeldSoort.Keuze"/>: de opties, één per regel.</summary>
|
||||
public string? Opties { get; set; }
|
||||
|
||||
public string? Hulptekst { get; set; }
|
||||
public bool Verplicht { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Een niet-actief veld staat niet meer in nieuwe formulieren, maar wat er al is ingevuld
|
||||
/// blijft staan en wordt bij het wijzigen van zo'n item nog getoond.
|
||||
/// </summary>
|
||||
public bool Actief { get; set; } = true;
|
||||
|
||||
public int Volgorde { get; set; } = 1000;
|
||||
|
||||
public VaultCategoryDefinition? Category { get; set; }
|
||||
|
||||
/// <summary>De opties als lijst, zonder lege regels.</summary>
|
||||
public IReadOnlyList<string> OptieLijst =>
|
||||
string.IsNullOrWhiteSpace(Opties)
|
||||
? Array.Empty<string>()
|
||||
: Opties.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
/// <summary>De soorten invoerveld die een formulier kent.</summary>
|
||||
public static class VeldSoort
|
||||
{
|
||||
/// <summary>Eén regel tekst.</summary>
|
||||
public const string Tekst = "tekst";
|
||||
|
||||
/// <summary>Meerdere regels tekst.</summary>
|
||||
public const string Lang = "lang";
|
||||
|
||||
/// <summary>Een datum; wordt als jjjj-mm-dd bewaard.</summary>
|
||||
public const string Datum = "datum";
|
||||
|
||||
/// <summary>Kiezen uit een lijst, bijvoorbeeld Ja / Nee / Alleen na overleg.</summary>
|
||||
public const string Keuze = "keuze";
|
||||
|
||||
/// <summary>Geen invoer maar een stukje uitleg tussen de velden.</summary>
|
||||
public const string Uitleg = "uitleg";
|
||||
|
||||
public static readonly string[] Alle = { Tekst, Lang, Datum, Keuze, Uitleg };
|
||||
|
||||
public static bool Bekend(string? soort) => soort is not null && Alle.Contains(soort);
|
||||
|
||||
/// <summary>Hoe de soort op het scherm heet, voor in het beheerscherm.</summary>
|
||||
public static string Naam(string soort) => soort switch
|
||||
{
|
||||
Tekst => "Korte tekst",
|
||||
Lang => "Lange tekst",
|
||||
Datum => "Datum",
|
||||
Keuze => "Keuzelijst",
|
||||
Uitleg => "Uitlegtekst (geen invoer)",
|
||||
_ => soort
|
||||
};
|
||||
}
|
||||
@@ -73,7 +73,8 @@ public enum AuditAction
|
||||
OverlijdenGemeld = 50, OverlijdenBetwist = 51, OverlijdenBevestigd = 52,
|
||||
NalatenschapToegangVerleend = 60,
|
||||
AdvertentieIngediend = 70, AdvertentieBetaald = 71, AdvertentieGoedgekeurd = 72,
|
||||
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74
|
||||
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74,
|
||||
KluisSoortBeheerd = 80
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -93,6 +93,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IFamilyRepository, FamilyRepository>();
|
||||
services.AddScoped<IInvitationRepository, InvitationRepository>();
|
||||
services.AddScoped<IVaultRepository, VaultRepository>();
|
||||
services.AddScoped<IVaultCategoryRepository, VaultCategoryRepository>();
|
||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
services.AddScoped<ILegacyRepository, LegacyRepository>();
|
||||
services.AddScoped<IToestelRepository, ToestelRepository>();
|
||||
@@ -103,6 +104,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IAuditLog, EfAuditLog>();
|
||||
|
||||
services.AddScoped<AccountService>();
|
||||
services.AddScoped<VaultCategoryService>();
|
||||
services.AddScoped<VaultService>();
|
||||
// De link naar de landingspagina voor iemand zonder account moet naar de echte site wijzen.
|
||||
services.AddScoped(sp =>
|
||||
|
||||
@@ -18,6 +18,8 @@ public class MijnBesluitDbContext : DbContext
|
||||
public DbSet<Invitation> Invitations => Set<Invitation>();
|
||||
public DbSet<VaultItem> VaultItems => Set<VaultItem>();
|
||||
public DbSet<VaultFile> VaultFiles => Set<VaultFile>();
|
||||
public DbSet<VaultCategoryDefinition> VaultCategories => Set<VaultCategoryDefinition>();
|
||||
public DbSet<VaultCategoryField> VaultCategoryFields => Set<VaultCategoryField>();
|
||||
public DbSet<Toestel> Toestellen => Set<Toestel>();
|
||||
public DbSet<Sessie> Sessies => Set<Sessie>();
|
||||
public DbSet<ContentGrant> ContentGrants => Set<ContentGrant>();
|
||||
@@ -218,6 +220,37 @@ public class MijnBesluitDbContext : DbContext
|
||||
e.HasOne(x => x.VaultFile).WithMany().HasForeignKey(x => x.VaultFileId);
|
||||
});
|
||||
|
||||
b.Entity<VaultCategoryDefinition>(e =>
|
||||
{
|
||||
e.ToTable("vault_categories");
|
||||
e.Property(x => x.Id).HasColumnName("id");
|
||||
e.Property(x => x.Naam).HasColumnName("naam").HasMaxLength(80).IsRequired();
|
||||
e.Property(x => x.Uitleg).HasColumnName("uitleg").HasMaxLength(500);
|
||||
e.Property(x => x.Actief).HasColumnName("actief");
|
||||
e.Property(x => x.Volgorde).HasColumnName("volgorde");
|
||||
e.Property(x => x.BesprekenMet).HasColumnName("bespreken_met");
|
||||
e.Property(x => x.Ingebouwd).HasColumnName("ingebouwd");
|
||||
e.Property(x => x.AangemaaktOp).HasColumnName("aangemaakt_op");
|
||||
e.Property(x => x.GewijzigdOp).HasColumnName("gewijzigd_op");
|
||||
e.HasMany(x => x.Velden).WithOne(v => v.Category).HasForeignKey(v => v.CategoryId);
|
||||
});
|
||||
|
||||
b.Entity<VaultCategoryField>(e =>
|
||||
{
|
||||
e.ToTable("vault_category_fields");
|
||||
e.Property(x => x.Id).HasColumnName("id");
|
||||
e.Property(x => x.CategoryId).HasColumnName("category_id");
|
||||
e.Property(x => x.Sleutel).HasColumnName("sleutel").HasMaxLength(40).IsRequired();
|
||||
e.Property(x => x.Label).HasColumnName("label").HasMaxLength(100).IsRequired();
|
||||
e.Property(x => x.Soort).HasColumnName("soort").HasMaxLength(10).IsRequired();
|
||||
e.Property(x => x.Opties).HasColumnName("opties").HasMaxLength(1000);
|
||||
e.Property(x => x.Hulptekst).HasColumnName("hulptekst").HasMaxLength(300);
|
||||
e.Property(x => x.Verplicht).HasColumnName("verplicht");
|
||||
e.Property(x => x.Actief).HasColumnName("actief");
|
||||
e.Property(x => x.Volgorde).HasColumnName("volgorde");
|
||||
e.Ignore(x => x.OptieLijst);
|
||||
});
|
||||
|
||||
b.Entity<ContentGrant>(e =>
|
||||
{
|
||||
e.ToTable("content_grants");
|
||||
|
||||
@@ -229,6 +229,44 @@ public class InvitationRepository : IInvitationRepository
|
||||
public void Remove(Invitation invitation) => _db.Invitations.Remove(invitation);
|
||||
}
|
||||
|
||||
/// <summary>De soorten kluisitems; zie <see cref="IVaultCategoryRepository"/>.</summary>
|
||||
public class VaultCategoryRepository : IVaultCategoryRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public VaultCategoryRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<VaultCategoryDefinition>> ListAsync(CancellationToken ct = default) =>
|
||||
await _db.VaultCategories.Include(c => c.Velden)
|
||||
.OrderBy(c => c.Volgorde).ThenBy(c => c.Naam).ToListAsync(ct);
|
||||
|
||||
public Task<VaultCategoryDefinition?> FindAsync(int id, CancellationToken ct = default) =>
|
||||
_db.VaultCategories.Include(c => c.Velden).FirstOrDefaultAsync(c => c.Id == id, ct);
|
||||
|
||||
public Task<VaultCategoryField?> FindFieldAsync(long fieldId, CancellationToken ct = default) =>
|
||||
_db.VaultCategoryFields.FirstOrDefaultAsync(f => f.Id == fieldId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<SoortGebruik>> GebruikAsync(CancellationToken ct = default) =>
|
||||
await _db.VaultItems.Where(v => v.DeletedAt == null)
|
||||
.GroupBy(v => v.Category)
|
||||
.Select(g => new SoortGebruik((int)g.Key, g.Count(), g.Select(v => v.OwnerPersonId).Distinct().Count()))
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlySet<int>> InGebruikDoorAsync(long personId, CancellationToken ct = default)
|
||||
{
|
||||
var lijst = await _db.VaultItems.Where(v => v.OwnerPersonId == personId && v.DeletedAt == null)
|
||||
.Select(v => v.Category).Distinct().ToListAsync(ct);
|
||||
return lijst.Select(c => (int)c).ToHashSet();
|
||||
}
|
||||
|
||||
public Task<int> AantalItemsAsync(int categoryId, CancellationToken ct = default) =>
|
||||
_db.VaultItems.CountAsync(v => v.Category == (VaultCategory)categoryId && v.DeletedAt == null, ct);
|
||||
|
||||
public void Add(VaultCategoryDefinition soort) => _db.VaultCategories.Add(soort);
|
||||
public void Remove(VaultCategoryDefinition soort) => _db.VaultCategories.Remove(soort);
|
||||
public void Add(VaultCategoryField veld) => _db.VaultCategoryFields.Add(veld);
|
||||
public void Remove(VaultCategoryField veld) => _db.VaultCategoryFields.Remove(veld);
|
||||
}
|
||||
|
||||
public class VaultRepository : IVaultRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
|
||||
@@ -98,6 +98,19 @@ public static class SocketActions
|
||||
/// <summary>Kluisitems die iemand rechtstreeks met jou heeft gedeeld, vooraf te bespreken.</summary>
|
||||
public const string VaultSharedWithMe = "vault.shared-with-me";
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems die deze gebruiker te zien krijgt, met bij een door de beheerder gemaakte
|
||||
/// soort de velden van het formulier. Een oudere server kent dit niet; de app valt dan terug op
|
||||
/// de ingebouwde lijst.
|
||||
/// </summary>
|
||||
public const string VaultCategories = "vault.categories";
|
||||
|
||||
/// <summary>
|
||||
/// Eén soort op nummer, met zijn velden, ook als die soort niet meer wordt aangeboden. Voor het
|
||||
/// lezen van het item van een ander, waar de lijst van jezelf de soort niet hoeft te bevatten.
|
||||
/// </summary>
|
||||
public const string VaultCategory = "vault.category";
|
||||
|
||||
/// <summary>Nalatenschappen die aan jou zijn toevertrouwd, en wat daarvan is vrijgegeven.</summary>
|
||||
public const string LegacyEntrusted = "legacy.entrusted";
|
||||
public const string LegacyEntrustedItems = "legacy.entrusted-items";
|
||||
|
||||
@@ -15,6 +15,12 @@ public class RequestScope
|
||||
public required LegacyService Legacy { get; init; }
|
||||
public required OverzichtService Overzicht { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems. Optioneel: zonder deze dienst werkt de kluis met de ingebouwde soorten
|
||||
/// en krijgen items geen soortnaam mee, en de app valt terug op haar eigen lijst.
|
||||
/// </summary>
|
||||
public VaultCategoryService? Soorten { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Voor het samenvoegen van dubbele profielen. Optioneel: zonder deze dienst werkt alles,
|
||||
/// alleen wordt een tweede uitnodiging dan geweigerd in plaats van samengevoegd.
|
||||
@@ -76,6 +82,17 @@ public class SocketDispatcher
|
||||
f.Id, f.FileName, f.ContentType, f.SizeBytes, f.CreatedAt, f.IsProfilePhoto
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// De namen van alle soorten op nummer, om onder elk kluisitem te kunnen zetten hoe het heet.
|
||||
/// Een soort die de beheerder zelf maakte staat niet in de lijst van de app zelf, dus de naam
|
||||
/// reist met het item mee. Zonder soortendienst is de lijst leeg en valt de app terug.
|
||||
/// </summary>
|
||||
private static async Task<IReadOnlyDictionary<int, string>> SoortNamenAsync(RequestScope scope, CancellationToken ct) =>
|
||||
scope.Soorten is null ? new Dictionary<int, string>() : await scope.Soorten.NamenAsync(ct);
|
||||
|
||||
private static string? SoortNaam(IReadOnlyDictionary<int, string> namen, int soort) =>
|
||||
namen.TryGetValue(soort, out var naam) ? naam : null;
|
||||
|
||||
/// <summary>Voor als er geen klok wordt meegegeven; de overdracht heeft er wel een nodig.</summary>
|
||||
private sealed class SysteemKlok : MijnBesluit.Application.Abstractions.IClock
|
||||
{
|
||||
@@ -451,12 +468,31 @@ public class SocketDispatcher
|
||||
{
|
||||
var p = request.PayloadAs<VaultListPayload>();
|
||||
var items = await scope.Vault.ListAsync(session!.PersonId, p?.Category, ct);
|
||||
var namen = await SoortNamenAsync(scope, ct);
|
||||
return SocketResponse.Ok(id, items.Select(i => new
|
||||
{
|
||||
i.Id, i.Title, i.Category, i.ReleasePolicy, i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId
|
||||
i.Id, i.Title, i.Category, i.ReleasePolicy, i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId,
|
||||
CategoryName = SoortNaam(namen, (int)i.Category)
|
||||
}));
|
||||
}
|
||||
|
||||
case SocketActions.VaultCategory:
|
||||
{
|
||||
var p = request.PayloadAs<IdPayload>();
|
||||
if (p is null) return SocketResponse.Error(id, "Geen soort opgegeven.");
|
||||
var soort = scope.Soorten is null ? null : await scope.Soorten.SoortAsync((int)p.Id, ct);
|
||||
return soort is null ? SocketResponse.Error(id, "Deze soort bestaat niet.") : SocketResponse.Ok(id, soort);
|
||||
}
|
||||
|
||||
case SocketActions.VaultCategories:
|
||||
{
|
||||
// Zonder soortendienst een lege lijst: de app valt dan terug op haar ingebouwde soorten.
|
||||
var lijst = scope.Soorten is null
|
||||
? Array.Empty<KluisSoort>()
|
||||
: (await scope.Soorten.ZichtbaarVoorAsync(session!.PersonId, ct)).ToArray();
|
||||
return SocketResponse.Ok(id, lijst);
|
||||
}
|
||||
|
||||
case SocketActions.VaultOpen:
|
||||
{
|
||||
var p = request.PayloadAs<IdPayload>();
|
||||
@@ -807,10 +843,12 @@ public class SocketDispatcher
|
||||
var r = await scope.Legacy.NalatenschapItemsAsync(p.PersonId, session!.UserId, ct);
|
||||
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
||||
|
||||
var nalatenschapNamen = await SoortNamenAsync(scope, ct);
|
||||
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
||||
{
|
||||
i.Id, i.Title, Category = (int)i.Category, ReleasePolicy = (int)i.ReleasePolicy,
|
||||
i.ReleaseAt, i.CreatedAt
|
||||
i.ReleaseAt, i.CreatedAt,
|
||||
CategoryName = SoortNaam(nalatenschapNamen, (int)i.Category)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -828,10 +866,12 @@ public class SocketDispatcher
|
||||
var r = await scope.Vault.ListSharedWithMeAsync(session!.UserId, ct);
|
||||
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
||||
|
||||
var gedeeldNamen = await SoortNamenAsync(scope, ct);
|
||||
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
||||
{
|
||||
i.Id, i.Title, Category = (int)i.Category, ReleasePolicy = (int)i.ReleasePolicy,
|
||||
i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId
|
||||
i.ReleaseAt, i.UpdatedAt, i.SharedManagerPersonId,
|
||||
CategoryName = SoortNaam(gedeeldNamen, (int)i.Category)
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ public class SocketServerService : BackgroundService
|
||||
Accounts = scope.ServiceProvider.GetRequiredService<AccountService>(),
|
||||
Families = scope.ServiceProvider.GetRequiredService<FamilyService>(),
|
||||
Vault = scope.ServiceProvider.GetRequiredService<VaultService>(),
|
||||
Soorten = scope.ServiceProvider.GetRequiredService<VaultCategoryService>(),
|
||||
Messages = scope.ServiceProvider.GetRequiredService<MessageService>(),
|
||||
Legacy = scope.ServiceProvider.GetRequiredService<LegacyService>(),
|
||||
Meldingen = scope.ServiceProvider.GetRequiredService<MeldingService>(),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using MijnBesluit.Application.Models;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
using MijnBesluit.Domain.Rules;
|
||||
|
||||
namespace MijnBesluit.Web.Components.Onderdelen;
|
||||
|
||||
@@ -14,8 +16,11 @@ namespace MijnBesluit.Web.Components.Onderdelen;
|
||||
/// </summary>
|
||||
public static class Kluisweergave
|
||||
{
|
||||
/// <summary>De ingevulde velden van een item, in de volgorde waarin je ze wilt lezen.</summary>
|
||||
public static IEnumerable<(string Label, string Waarde)> Velden(VaultPayload p)
|
||||
/// <summary>
|
||||
/// De ingevulde velden van een item, in de volgorde waarin je ze wilt lezen. Bij een soort die
|
||||
/// de beheerder zelf maakte staan de eigen velden erbij; geef daarvoor de soort mee.
|
||||
/// </summary>
|
||||
public static IEnumerable<(string Label, string Waarde)> Velden(VaultPayload p, KluisSoort? soort = null)
|
||||
{
|
||||
if (p.Website is not null) yield return ("Website", p.Website);
|
||||
if (p.Username is not null) yield return ("Gebruikersnaam", p.Username);
|
||||
@@ -33,9 +38,35 @@ public static class Kluisweergave
|
||||
if (p.Music is not null) yield return ("Muziek", p.Music);
|
||||
if (p.Speakers is not null) yield return ("Sprekers", p.Speakers);
|
||||
if (p.Wishes is not null) yield return ("Wensen", p.Wishes);
|
||||
foreach (var eigen in EigenVelden(p, soort)) yield return eigen;
|
||||
if (p.Notes is not null) yield return ("Toelichting", p.Notes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// De velden van een soort die de beheerder zelf maakte, met de naam die hij ze gaf. Alleen wat
|
||||
/// is ingevuld; ook een veld dat inmiddels niet-actief is blijft zichtbaar zolang er iets in staat.
|
||||
/// </summary>
|
||||
public static IEnumerable<(string Label, string Waarde)> EigenVelden(VaultPayload p, KluisSoort? soort)
|
||||
{
|
||||
if (soort is null || p.Velden is null) yield break;
|
||||
|
||||
foreach (var veld in soort.Velden.Where(v => v.Soort != VeldSoort.Uitleg))
|
||||
{
|
||||
if (!p.Velden.TryGetValue(veld.Sleutel, out var waarde) || string.IsNullOrWhiteSpace(waarde)) continue;
|
||||
|
||||
if (veld.Soort == VeldSoort.Datum &&
|
||||
DateOnly.TryParseExact(waarde, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None, out var dag))
|
||||
waarde = $"{dag.Day} {Datums.Maanden[dag.Month - 1]} {dag.Year}";
|
||||
|
||||
yield return (veld.Label, waarde);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>De naam van een soort: die van de beheerder als de server hem kent, anders de vaste.</summary>
|
||||
public static string Naam(VaultCategory c, IReadOnlyDictionary<int, string>? namen) =>
|
||||
namen is not null && namen.TryGetValue((int)c, out var naam) ? naam : Naam(c);
|
||||
|
||||
public static string Naam(VaultCategory c) => c switch
|
||||
{
|
||||
VaultCategory.Uitvaartwens => "Uitvaartwens",
|
||||
|
||||
@@ -11,7 +11,16 @@
|
||||
Nieuwe of wezenlijk gewijzigde functies, nieuwste bovenaan.
|
||||
</p>
|
||||
|
||||
<h2 style="margin-top:0">20 september 2026</h2>
|
||||
<h2 style="margin-top:0">20 oktober 2026 · 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>
|
||||
|
||||
<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
|
||||
|
||||
@@ -31,6 +31,10 @@ else
|
||||
hoge uitzondering. *@
|
||||
<p><a href="/beheer/adverteren">Advertenties beheren ›</a></p>
|
||||
|
||||
@* Kluissoorten: wat mensen in hun kluis kunnen vastleggen, en wat je daar zelf bij zet.
|
||||
Ook een eigen scherm: dat is inrichten, geen terugdraaien. *@
|
||||
<p><a href="/beheer/soorten">Kluissoorten beheren ›</a></p>
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
@* ------------------------------------------------------------ statistieken *@
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
@page "/beheer/soorten"
|
||||
@attribute [Authorize]
|
||||
@inject BeheerService Beheerkant
|
||||
@inject VaultCategoryService SoortService
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Kluissoorten - Beheer</PageTitle>
|
||||
|
||||
@* Het beheerscherm voor de soorten kluisitems: wat mensen in hun kluis kunnen vastleggen
|
||||
(Wachtwoord, Polis, Uitvaartwens, en wat je er zelf bij zet, zoals "Niet reanimeren").
|
||||
|
||||
Drie regels, in de volgorde waarin ze ertoe doen:
|
||||
1. Wat in gebruik is, verdwijnt nooit. Een soort met items is alleen op niet-actief te
|
||||
zetten; wie hem al gebruikt blijft hem zien, wie niet ziet hem niet meer.
|
||||
2. Je ziet hier aantallen, nooit inhoud of namen. Hoe vaak is een tel, wie en wat niet.
|
||||
3. Een nieuwe soort vraagt geen nieuwe versie van de app: de app tekent het formulier naar de
|
||||
velden die je hier opgeeft.
|
||||
|
||||
Zie docs/Kluissoorten.md. *@
|
||||
|
||||
@if (!Magdit)
|
||||
{
|
||||
<h1>Niet gevonden</h1>
|
||||
<p class="zacht">Deze pagina bestaat niet, of hij is niet voor jou.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1>Kluissoorten</h1>
|
||||
<p class="zacht">
|
||||
Wat mensen in hun kluis kunnen vastleggen. Een nieuwe soort verschijnt meteen in de app en op
|
||||
de website, zonder dat er een nieuwe versie van de app nodig is. Bij elke soort zie je hoe
|
||||
vaak hij gebruikt wordt; wat erin staat zie je nooit.
|
||||
</p>
|
||||
<p><a href="/beheer">‹ Terug naar beheer</a></p>
|
||||
|
||||
<div class="melding let-op">
|
||||
<strong>De naam van een soort is leesbaar voor de server.</strong> Alleen de inhoud van een
|
||||
kluisitem is versleuteld, de naam van de soort niet. Wie "Dementieverklaring" als soort
|
||||
aanmaakt, zet dat woord dus in de database. Kies een neutrale naam als de soort zelf iets
|
||||
over iemands gezondheid zegt, en zet het gevoelige deel in de velden: die waarden blijven
|
||||
versleuteld.
|
||||
</div>
|
||||
|
||||
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||
|
||||
@* ------------------------------------------------------------ nieuwe soort *@
|
||||
<div class="kaart">
|
||||
<h2 style="margin-top:0">Nieuwe soort</h2>
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label for="nieuwenaam">Naam</label>
|
||||
<input id="nieuwenaam" type="text" maxlength="80" @bind="NieuweSoort.Naam" @bind:event="oninput"
|
||||
placeholder="Bijvoorbeeld: Niet reanimeren" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="nieuweuitleg">Uitleg onder de keuzelijst (mag leeg)</label>
|
||||
<input id="nieuweuitleg" type="text" maxlength="500" @bind="NieuweSoort.Uitleg" @bind:event="oninput" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="keuze">
|
||||
<input type="checkbox" id="nieuwbespreken" @bind="NieuweSoort.BesprekenMet" />
|
||||
<label for="nieuwbespreken">Bij deze soort kan de eigenaar iemand kiezen om het vooraf mee te bespreken</label>
|
||||
</div>
|
||||
<div class="rij" style="margin-top:.8rem">
|
||||
<button class="knop" disabled="@Bezig" @onclick="Aanmaken">Soort toevoegen</button>
|
||||
</div>
|
||||
<p class="zacht klein" style="margin-bottom:0">
|
||||
Na het toevoegen kun je de velden van het formulier invullen en bepalen of de soort
|
||||
actief is. Een soort zonder velden toont alleen de titel en de toelichting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@* ------------------------------------------------------------ de lijst *@
|
||||
@if (Lijst is null)
|
||||
{
|
||||
<p class="zacht">Bezig met laden…</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="stapel">
|
||||
@foreach (var s in Lijst)
|
||||
{
|
||||
<div class="kaart">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<div class="groot">@s.Naam</div>
|
||||
<div class="zacht">@Gebruik(s)</div>
|
||||
<div style="margin-top:.35rem;display:flex;gap:.4rem;flex-wrap:wrap">
|
||||
@if (s.Ingebouwd) { <span class="label">Ingebouwd</span> }
|
||||
else { <span class="label geclaimd">Eigen soort</span> }
|
||||
@if (!s.Actief) { <span class="label uitgenodigd">Niet actief</span> }
|
||||
@if (s.BesprekenMet) { <span class="label">Bespreken met</span> }
|
||||
</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => Openen(s)">
|
||||
@(Open == s.Id ? "Sluiten" : "Beheren")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (Open == s.Id)
|
||||
{
|
||||
<hr />
|
||||
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label>Naam</label>
|
||||
<input type="text" maxlength="80" @bind="Bewerken.Naam" @bind:event="oninput" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label>Plek in de lijst</label>
|
||||
<input type="number" @bind="Bewerken.Volgorde" />
|
||||
<div class="uitleg">Kleinste nummer staat bovenaan.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label>Uitleg onder de keuzelijst</label>
|
||||
<input type="text" maxlength="500" @bind="Bewerken.Uitleg" @bind:event="oninput" />
|
||||
</div>
|
||||
<div class="keuze">
|
||||
<input type="checkbox" id="actief@(s.Id)" @bind="Bewerken.Actief" />
|
||||
<label for="actief@(s.Id)">Actief: nieuwe gebruikers kunnen deze soort kiezen</label>
|
||||
</div>
|
||||
@if (!Bewerken.Actief)
|
||||
{
|
||||
<p class="zacht klein" style="margin:.2rem 0 .6rem 1.7rem">
|
||||
Wie al een item van deze soort heeft, blijft hem zien en kan zijn items gewoon
|
||||
bekijken en wijzigen. Voor iedereen die hem nog nooit gebruikte verdwijnt hij uit de lijst.
|
||||
</p>
|
||||
}
|
||||
<div class="keuze">
|
||||
<input type="checkbox" id="bespreken@(s.Id)" @bind="Bewerken.BesprekenMet" />
|
||||
<label for="bespreken@(s.Id)">Bespreken met: de eigenaar kan iemand kiezen om dit vooraf mee te bespreken</label>
|
||||
</div>
|
||||
|
||||
<div class="rij" style="margin-top:.8rem">
|
||||
<button class="knop" disabled="@Bezig" @onclick="() => Opslaan(s.Id)">Wijzigingen opslaan</button>
|
||||
@if (s.KanVerwijderen)
|
||||
{
|
||||
<button class="knop rustig" @onclick="() => WeghalenSoort = s.Id">Soort verwijderen</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!s.KanVerwijderen)
|
||||
{
|
||||
<p class="zacht klein" style="margin-bottom:0">
|
||||
@if (s.Ingebouwd) { <text>Een ingebouwde soort kun je niet verwijderen, alleen niet-actief maken.</text> }
|
||||
else { <text>Deze soort is in gebruik en kan dus niet worden verwijderd. Zet hem op niet-actief als je hem niet meer wilt aanbieden.</text> }
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (WeghalenSoort == s.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.9rem">
|
||||
<strong>@s.Naam</strong> verwijderen? Dit kan, omdat nog niemand er een item mee heeft gemaakt.
|
||||
<div class="rij" style="margin-top:.7rem">
|
||||
<button class="knop gevaar" @onclick="() => Verwijderen(s.Id)">Ja, verwijderen</button>
|
||||
<button class="knop rustig" @onclick="() => WeghalenSoort = null">Nee, bewaren</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<hr />
|
||||
<h3>Velden van het formulier</h3>
|
||||
|
||||
@if (s.Ingebouwd)
|
||||
{
|
||||
<p class="zacht">
|
||||
Het invulscherm van een ingebouwde soort zit in de app zelf en is hier niet te
|
||||
wijzigen. Wil je voor zoiets als "Polis" andere velden, maak dan een eigen soort.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (s.Velden.Count == 0)
|
||||
{
|
||||
<p class="zacht">Nog geen velden. Zonder velden vult iemand alleen een titel en een toelichting in.</p>
|
||||
}
|
||||
|
||||
<div class="stapel">
|
||||
@foreach (var v in s.Velden)
|
||||
{
|
||||
<div class="kaart" style="background:var(--vlak-zacht)">
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<div>@v.Label</div>
|
||||
<div class="zacht klein">
|
||||
@VeldSoort.Naam(v.Soort)
|
||||
@if (v.Verplicht) { <text> · verplicht</text> }
|
||||
@if (!v.Actief) { <text> · niet actief</text> }
|
||||
</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => VeldOpenen(v)">
|
||||
@(VeldBewerkId == v.Id ? "Sluiten" : "Wijzigen")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (VeldBewerkId == v.Id)
|
||||
{
|
||||
<div class="veld" style="margin-top:.8rem">
|
||||
<label>Naam van het veld</label>
|
||||
<input type="text" maxlength="100" @bind="VeldBewerken.Label" @bind:event="oninput" />
|
||||
<div class="uitleg">
|
||||
De naam hernoemen mag: wat mensen al hebben ingevuld blijft bij het veld horen.
|
||||
Het soort veld ligt vast.
|
||||
</div>
|
||||
</div>
|
||||
@if (v.Soort == VeldSoort.Keuze)
|
||||
{
|
||||
<div class="veld">
|
||||
<label>Keuzes, één per regel</label>
|
||||
<textarea rows="4" @bind="VeldBewerken.Opties" @bind:event="oninput"></textarea>
|
||||
</div>
|
||||
}
|
||||
<div class="veld">
|
||||
<label>@(v.Soort == VeldSoort.Uitleg ? "Tekst" : "Hulptekst onder het veld")</label>
|
||||
<input type="text" maxlength="300" @bind="VeldBewerken.Hulptekst" @bind:event="oninput" />
|
||||
</div>
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label>Plek in het formulier</label>
|
||||
<input type="number" @bind="VeldBewerken.Volgorde" />
|
||||
</div>
|
||||
<div>
|
||||
@if (v.Soort != VeldSoort.Uitleg)
|
||||
{
|
||||
<div class="keuze">
|
||||
<input type="checkbox" id="vv@(v.Id)" @bind="VeldBewerken.Verplicht" />
|
||||
<label for="vv@(v.Id)">Verplicht</label>
|
||||
</div>
|
||||
}
|
||||
<div class="keuze">
|
||||
<input type="checkbox" id="va@(v.Id)" @bind="VeldBewerken.Actief" />
|
||||
<label for="va@(v.Id)">Actief</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rij">
|
||||
<button class="knop" disabled="@Bezig" @onclick="() => VeldOpslaan(v.Id)">Veld opslaan</button>
|
||||
<button class="knop rustig" @onclick="() => WeghalenVeld = v.Id">Veld verwijderen</button>
|
||||
</div>
|
||||
<p class="zacht klein" style="margin-bottom:0">
|
||||
@if (s.Items > 0)
|
||||
{
|
||||
<text>Er zijn al items van deze soort: een veld verwijderen kan dan niet. Zet het op niet-actief; wat er is ingevuld blijft bewaard.</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Er zijn nog geen items van deze soort, dus je kunt het veld nog verwijderen.</text>
|
||||
}
|
||||
</p>
|
||||
|
||||
@if (WeghalenVeld == v.Id)
|
||||
{
|
||||
<div class="melding let-op" style="margin-top:.7rem">
|
||||
<strong>@v.Label</strong> verwijderen?
|
||||
<div class="rij" style="margin-top:.6rem">
|
||||
<button class="knop gevaar" @onclick="() => VeldVerwijderen(v.Id)">Ja, verwijderen</button>
|
||||
<button class="knop rustig" @onclick="() => WeghalenVeld = null">Nee, bewaren</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ---------------------------------------------------- veld toevoegen *@
|
||||
<h3 style="margin-top:1.2rem">Veld toevoegen</h3>
|
||||
<div class="raster twee">
|
||||
<div class="veld">
|
||||
<label>Naam van het veld</label>
|
||||
<input type="text" maxlength="100" @bind="NieuwVeld.Label" @bind:event="oninput"
|
||||
placeholder="Bijvoorbeeld: Wil je gereanimeerd worden?" />
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label>Soort veld</label>
|
||||
<select @bind="NieuwVeld.Soort">
|
||||
@foreach (var soort in VeldSoort.Alle)
|
||||
{
|
||||
<option value="@soort">@VeldSoort.Naam(soort)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@if (NieuwVeld.Soort == VeldSoort.Keuze)
|
||||
{
|
||||
<div class="veld">
|
||||
<label>Keuzes, één per regel</label>
|
||||
<textarea rows="4" @bind="NieuwVeld.Opties" @bind:event="oninput"
|
||||
placeholder="Ja Nee Alleen na overleg"></textarea>
|
||||
</div>
|
||||
}
|
||||
<div class="veld">
|
||||
<label>@(NieuwVeld.Soort == VeldSoort.Uitleg ? "Tekst" : "Hulptekst onder het veld (mag leeg)")</label>
|
||||
<input type="text" maxlength="300" @bind="NieuwVeld.Hulptekst" @bind:event="oninput" />
|
||||
</div>
|
||||
@if (NieuwVeld.Soort != VeldSoort.Uitleg)
|
||||
{
|
||||
<div class="keuze">
|
||||
<input type="checkbox" id="nieuwverplicht@(s.Id)" @bind="NieuwVeld.Verplicht" />
|
||||
<label for="nieuwverplicht@(s.Id)">Verplicht</label>
|
||||
</div>
|
||||
}
|
||||
<div class="rij" style="margin-top:.8rem">
|
||||
<button class="knop" disabled="@Bezig" @onclick="() => VeldToevoegen(s.Id)">Veld toevoegen</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<SoortBeheerbeeld>? Lijst;
|
||||
|
||||
/// <summary>Welke soort openstaat voor beheer.</summary>
|
||||
private int? Open;
|
||||
private int? WeghalenSoort;
|
||||
private long? VeldBewerkId, WeghalenVeld;
|
||||
|
||||
private SoortFormulier NieuweSoort = new();
|
||||
private SoortFormulier Bewerken = new();
|
||||
private VeldFormulier NieuwVeld = new();
|
||||
private VeldFormulier VeldBewerken = new();
|
||||
|
||||
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 SoortService.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 static string Aantal(int n, string een, string meer) => $"{n} {(n == 1 ? een : meer)}";
|
||||
|
||||
private static string Gebruik(SoortBeheerbeeld s) => s.Items == 0
|
||||
? "Nog nooit gebruikt"
|
||||
: $"Gebruikt in {Aantal(s.Items, "item", "items")} door {Aantal(s.Eigenaren, "persoon", "mensen")}";
|
||||
|
||||
private void Openen(SoortBeheerbeeld s)
|
||||
{
|
||||
if (Open == s.Id) { Open = null; return; }
|
||||
|
||||
Open = s.Id;
|
||||
WeghalenSoort = null;
|
||||
VeldBewerkId = null;
|
||||
WeghalenVeld = null;
|
||||
NieuwVeld = new VeldFormulier();
|
||||
Bewerken = new SoortFormulier
|
||||
{
|
||||
Naam = s.Naam, Uitleg = s.Uitleg ?? "", Actief = s.Actief,
|
||||
Volgorde = s.Volgorde, BesprekenMet = s.BesprekenMet
|
||||
};
|
||||
}
|
||||
|
||||
private async Task Aanmaken()
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var resultaat = await SoortService.MaakAsync(_gebruikerId, NieuweSoort.Naam, NieuweSoort.Uitleg,
|
||||
NieuweSoort.BesprekenMet);
|
||||
if (resultaat.Succeeded)
|
||||
{
|
||||
Meld($"{NieuweSoort.Naam.Trim()} is toegevoegd. Vul nu de velden in.", true);
|
||||
NieuweSoort = new SoortFormulier();
|
||||
await Laden();
|
||||
var gemaakt = Lijst?.FirstOrDefault(x => x.Id == resultaat.Value);
|
||||
if (gemaakt is not null) { Open = null; Openen(gemaakt); }
|
||||
}
|
||||
else Meld(resultaat.Error, false);
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task Opslaan(int id)
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var resultaat = await SoortService.WijzigAsync(_gebruikerId, id, Bewerken.Naam, Bewerken.Uitleg,
|
||||
Bewerken.Actief, Bewerken.Volgorde, Bewerken.BesprekenMet);
|
||||
if (resultaat.Succeeded) { Meld("Opgeslagen.", true); await Laden(); }
|
||||
else Meld(resultaat.Error, false);
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task Verwijderen(int id)
|
||||
{
|
||||
var resultaat = await SoortService.VerwijderAsync(_gebruikerId, id);
|
||||
WeghalenSoort = null;
|
||||
if (resultaat.Succeeded) { Open = null; Meld("De soort is verwijderd.", true); await Laden(); }
|
||||
else Meld(resultaat.Error, false);
|
||||
}
|
||||
|
||||
private void VeldOpenen(VeldBeheerbeeld v)
|
||||
{
|
||||
if (VeldBewerkId == v.Id) { VeldBewerkId = null; return; }
|
||||
|
||||
VeldBewerkId = v.Id;
|
||||
WeghalenVeld = null;
|
||||
VeldBewerken = new VeldFormulier
|
||||
{
|
||||
Label = v.Label, Soort = v.Soort, Opties = v.Opties ?? "", Hulptekst = v.Hulptekst ?? "",
|
||||
Verplicht = v.Verplicht, Actief = v.Actief, Volgorde = v.Volgorde
|
||||
};
|
||||
}
|
||||
|
||||
private async Task VeldToevoegen(int soortId)
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var resultaat = await SoortService.VeldToevoegenAsync(_gebruikerId, soortId, NieuwVeld.Label, NieuwVeld.Soort,
|
||||
NieuwVeld.Opties, NieuwVeld.Hulptekst, NieuwVeld.Verplicht);
|
||||
if (resultaat.Succeeded) { Meld($"{NieuwVeld.Label.Trim()} is toegevoegd.", true); NieuwVeld = new VeldFormulier(); await Laden(); }
|
||||
else Meld(resultaat.Error, false);
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task VeldOpslaan(long veldId)
|
||||
{
|
||||
Bezig = true;
|
||||
try
|
||||
{
|
||||
var resultaat = await SoortService.VeldWijzigenAsync(_gebruikerId, veldId, VeldBewerken.Label, VeldBewerken.Opties,
|
||||
VeldBewerken.Hulptekst, VeldBewerken.Verplicht,
|
||||
VeldBewerken.Actief, VeldBewerken.Volgorde);
|
||||
if (resultaat.Succeeded) { Meld("Het veld is opgeslagen.", true); VeldBewerkId = null; await Laden(); }
|
||||
else Meld(resultaat.Error, false);
|
||||
}
|
||||
finally { Bezig = false; }
|
||||
}
|
||||
|
||||
private async Task VeldVerwijderen(long veldId)
|
||||
{
|
||||
var resultaat = await SoortService.VeldVerwijderenAsync(_gebruikerId, veldId);
|
||||
WeghalenVeld = null;
|
||||
if (resultaat.Succeeded) { VeldBewerkId = null; Meld("Het veld is verwijderd.", true); await Laden(); }
|
||||
else Meld(resultaat.Error, false);
|
||||
}
|
||||
|
||||
public class SoortFormulier
|
||||
{
|
||||
public string Naam { get; set; } = "";
|
||||
public string Uitleg { get; set; } = "";
|
||||
public bool Actief { get; set; } = true;
|
||||
public int Volgorde { get; set; }
|
||||
public bool BesprekenMet { get; set; }
|
||||
}
|
||||
|
||||
public class VeldFormulier
|
||||
{
|
||||
public string Label { get; set; } = "";
|
||||
public string Soort { get; set; } = VeldSoort.Tekst;
|
||||
public string Opties { get; set; } = "";
|
||||
public string Hulptekst { get; set; } = "";
|
||||
public bool Verplicht { get; set; }
|
||||
public bool Actief { get; set; } = true;
|
||||
public int Volgorde { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
@attribute [Authorize]
|
||||
@inject VaultService KluisService
|
||||
@inject LegacyService BeheerService
|
||||
@inject VaultCategoryService SoortService
|
||||
@inject IJSRuntime JS
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@@ -21,9 +22,9 @@
|
||||
<label for="cat">Soort</label>
|
||||
<select id="cat" @bind="Filter" @bind:after="Verversen">
|
||||
<option value="">Alles</option>
|
||||
@foreach (var c in Categorieen)
|
||||
@foreach (var s in Soorten)
|
||||
{
|
||||
<option value="@((int)c)">@Naam(c)</option>
|
||||
<option value="@s.Id">@s.Naam</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
@@ -43,9 +44,13 @@
|
||||
OpBevestigen="Opslaan" OpSluiten="Annuleren">
|
||||
<div class="veld">
|
||||
<label for="soort">Wat wil je vastleggen?</label>
|
||||
<InputSelect id="soort" @bind-Value="Nieuw.Categorie">
|
||||
@foreach (var c in Categorieen) { <option value="@c">@Naam(c)</option> }
|
||||
<InputSelect id="soort" @bind-Value="Nieuw.CategorieNummer">
|
||||
@foreach (var s in Soorten) { <option value="@s.Id">@s.Naam</option> }
|
||||
</InputSelect>
|
||||
@if (!string.IsNullOrWhiteSpace(HuidigeSoort?.Uitleg))
|
||||
{
|
||||
<div class="uitleg">@HuidigeSoort!.Uitleg</div>
|
||||
}
|
||||
</div>
|
||||
<div class="veld">
|
||||
<label for="titel">Titel</label>
|
||||
@@ -129,9 +134,20 @@
|
||||
|
||||
case VaultCategory.Donorcodicil:
|
||||
break;
|
||||
|
||||
default:
|
||||
@* Een soort die de beheerder zelf heeft gemaakt: het formulier komt uit zijn velden. *@
|
||||
@if (HuidigeSoort is { Ingebouwd: false } eigen)
|
||||
{
|
||||
@foreach (var veld in eigen.Velden.Where(v => v.Actief || HeeftWaarde(v)))
|
||||
{
|
||||
@VeldInvoer(veld)
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@if (Nieuw.Categorie is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil)
|
||||
@if (BesprekenMetAan)
|
||||
{
|
||||
<div class="veld">
|
||||
<label>Bespreken met</label>
|
||||
@@ -256,7 +272,7 @@ else
|
||||
{
|
||||
<hr />
|
||||
<dl style="margin:0">
|
||||
@foreach (var (label, waarde) in Velden(Inhoud))
|
||||
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud, GeopendSoort))
|
||||
{
|
||||
<div class="rij" style="padding:.25rem 0">
|
||||
<span class="zacht" style="min-width:150px">@label</span>
|
||||
@@ -358,13 +374,85 @@ else
|
||||
/// <summary>Zoveel bestanden tegelijk kiezen mag. Blazor wil hier een bovengrens.</summary>
|
||||
private const int MaxTegelijk = 10;
|
||||
|
||||
private static readonly VaultCategory[] Categorieen =
|
||||
/// <summary>
|
||||
/// De soorten die deze persoon te zien krijgt: de actieve, plus de niet-actieve waar hij al een
|
||||
/// item van heeft. De lijst komt van de server en niet meer uit de code, zodat de beheerder er
|
||||
/// een soort bij kan zetten zonder dat er iets uitgerold hoeft te worden.
|
||||
/// </summary>
|
||||
private IReadOnlyList<KluisSoort> Soorten = Array.Empty<KluisSoort>();
|
||||
|
||||
/// <summary>Alle namen op nummer, ook van soorten die niet meer actief zijn.</summary>
|
||||
private IReadOnlyDictionary<int, string> Namen = new Dictionary<int, string>();
|
||||
|
||||
/// <summary>De soort van het item dat openstaat, voor de namen van zijn eigen velden.</summary>
|
||||
private KluisSoort? GeopendSoort;
|
||||
|
||||
private KluisSoort? HuidigeSoort => Soorten.FirstOrDefault(s => s.Id == (int)Nieuw.Categorie);
|
||||
|
||||
/// <summary>
|
||||
/// "Bespreken met": bij een eigen soort bepaalt de beheerder dat per soort. Zonder soortenlijst
|
||||
/// (die is er altijd, maar voor de zekerheid) geldt de oude regel.
|
||||
/// </summary>
|
||||
private bool BesprekenMetAan =>
|
||||
HuidigeSoort?.BesprekenMet ?? Nieuw.Categorie is VaultCategory.Uitvaartwens or VaultCategory.Donorcodicil;
|
||||
|
||||
private async Task SoortenLaden()
|
||||
{
|
||||
VaultCategory.Uitvaartwens, VaultCategory.Donorcodicil, VaultCategory.Polis, VaultCategory.Wachtwoord,
|
||||
VaultCategory.SocialMedia, VaultCategory.Apparaat, VaultCategory.Pincode,
|
||||
VaultCategory.Rijbewijs, VaultCategory.Paspoort,
|
||||
VaultCategory.Document, VaultCategory.Notitie, VaultCategory.Overig
|
||||
};
|
||||
Soorten = await SoortService.ZichtbaarVoorAsync(_persoonId);
|
||||
Namen = await SoortService.NamenAsync();
|
||||
}
|
||||
|
||||
/// <summary>Een leeg nieuw item, in de eerste soort die bruikbaar is (liefst uitvaartwens).</summary>
|
||||
private NieuwItem LeegItem()
|
||||
{
|
||||
var soort = Soorten.FirstOrDefault(s => s.Id == (int)VaultCategory.Uitvaartwens) ?? Soorten.FirstOrDefault();
|
||||
return soort is null ? new NieuwItem() : new NieuwItem { CategorieNummer = soort.Id };
|
||||
}
|
||||
|
||||
private string Waarde(string sleutel) => Nieuw.Velden.TryGetValue(sleutel, out var w) ? w : "";
|
||||
private void Zet(string sleutel, string? waarde) => Nieuw.Velden[sleutel] = waarde ?? "";
|
||||
private bool HeeftWaarde(KluisVeld veld) => !string.IsNullOrWhiteSpace(Waarde(veld.Sleutel));
|
||||
|
||||
/// <summary>Eén invoerveld van een eigen soort, getekend naar de beschrijving van de beheerder.</summary>
|
||||
private RenderFragment VeldInvoer(KluisVeld veld) => @<div class="veld">
|
||||
@if (veld.Soort == VeldSoort.Uitleg)
|
||||
{
|
||||
<div class="uitleg">@veld.Hulptekst</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label>@veld.Label@(veld.Verplicht ? " *" : "")</label>
|
||||
@if (veld.Soort == VeldSoort.Lang)
|
||||
{
|
||||
<textarea rows="4" value="@Waarde(veld.Sleutel)"
|
||||
@oninput="e => Zet(veld.Sleutel, e.Value?.ToString())"></textarea>
|
||||
}
|
||||
else if (veld.Soort == VeldSoort.Datum)
|
||||
{
|
||||
<input type="date" value="@Waarde(veld.Sleutel)"
|
||||
@onchange="e => Zet(veld.Sleutel, e.Value?.ToString())" />
|
||||
}
|
||||
else if (veld.Soort == VeldSoort.Keuze)
|
||||
{
|
||||
<select @onchange="e => Zet(veld.Sleutel, e.Value?.ToString())">
|
||||
<option value="" selected="@(Waarde(veld.Sleutel) == "")">Nog niet gekozen</option>
|
||||
@foreach (var optie in veld.Opties)
|
||||
{
|
||||
<option value="@optie" selected="@(Waarde(veld.Sleutel) == optie)">@optie</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" value="@Waarde(veld.Sleutel)"
|
||||
@oninput="e => Zet(veld.Sleutel, e.Value?.ToString())" />
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(veld.Hulptekst))
|
||||
{
|
||||
<div class="uitleg">@veld.Hulptekst</div>
|
||||
}
|
||||
}
|
||||
</div>;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -382,6 +470,8 @@ else
|
||||
_gebruikerId = user.UserId() ?? 0;
|
||||
_persoonId = user.PersonId() ?? 0;
|
||||
Beheerders = await BeheerService.ListManagersAsync(_persoonId);
|
||||
await SoortenLaden();
|
||||
Nieuw = LeegItem();
|
||||
await Verversen();
|
||||
}
|
||||
|
||||
@@ -402,6 +492,8 @@ else
|
||||
{
|
||||
Geopend = id;
|
||||
Inhoud = resultaat.Value;
|
||||
var gekozen = Items?.FirstOrDefault(i => i.Id == id);
|
||||
GeopendSoort = gekozen is null ? null : await SoortService.SoortAsync((int)gekozen.Category);
|
||||
Melding = null;
|
||||
await BijlagenLaden();
|
||||
}
|
||||
@@ -514,6 +606,7 @@ else
|
||||
Muziek = p.Music ?? "",
|
||||
Sprekers = p.Speakers ?? "",
|
||||
Wensen = p.Wishes ?? "",
|
||||
Velden = p.Velden is null ? new Dictionary<string, string>() : new Dictionary<string, string>(p.Velden),
|
||||
BesprekenMetPersonId = item.SharedManagerPersonId,
|
||||
Vrijgave = item.ReleasePolicy,
|
||||
VrijgaveDatum = item.ReleaseAt is { } r ? DateOnly.FromDateTime(r) : null
|
||||
@@ -533,7 +626,7 @@ else
|
||||
private void Beginnen()
|
||||
{
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwItem();
|
||||
Nieuw = LeegItem();
|
||||
Bijlagen = null;
|
||||
ToonFormulier = true;
|
||||
}
|
||||
@@ -541,7 +634,7 @@ else
|
||||
private void Annuleren()
|
||||
{
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwItem();
|
||||
Nieuw = LeegItem();
|
||||
Bijlagen = null;
|
||||
ToonFormulier = false;
|
||||
}
|
||||
@@ -575,7 +668,13 @@ else
|
||||
Location = Leeg(Nieuw.Locatie),
|
||||
Music = Leeg(Nieuw.Muziek),
|
||||
Speakers = Leeg(Nieuw.Sprekers),
|
||||
Wishes = Leeg(Nieuw.Wensen)
|
||||
Wishes = Leeg(Nieuw.Wensen),
|
||||
// Alleen voor een soort die de beheerder zelf maakte. Wat hier ook staat, de server
|
||||
// houdt alleen de velden over die bij de soort horen en controleert ze nog eens.
|
||||
Velden = HuidigeSoort is { Ingebouwd: false }
|
||||
? Nieuw.Velden.Where(v => !string.IsNullOrWhiteSpace(v.Value))
|
||||
.ToDictionary(v => v.Key, v => v.Value)
|
||||
: null
|
||||
};
|
||||
|
||||
DateTime? datum = Nieuw.Vrijgave == ReleasePolicy.OpDatum && Nieuw.VrijgaveDatum is { } d
|
||||
@@ -600,9 +699,10 @@ else
|
||||
Melding = BewerkId == 0 ? "Opgeslagen in je kluis." : "De wijziging is opgeslagen.";
|
||||
MeldingSoort = "goed";
|
||||
BewerkId = 0;
|
||||
Nieuw = new NieuwItem();
|
||||
Nieuw = LeegItem();
|
||||
Bijlagen = null;
|
||||
ToonFormulier = false;
|
||||
await SoortenLaden();
|
||||
await Verversen();
|
||||
}
|
||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||
@@ -615,8 +715,7 @@ else
|
||||
|
||||
// Velden() en Naam() staan in Onderdelen/Kluisweergave.cs: de nalatenschapspagina toont
|
||||
// precies hetzelfde, en twee kopieën van die lijst lopen vroeg of laat uit elkaar.
|
||||
private static IEnumerable<(string, string)> Velden(VaultPayload p) => Kluisweergave.Velden(p);
|
||||
private static string Naam(VaultCategory c) => Kluisweergave.Naam(c);
|
||||
private string Naam(VaultCategory c) => Kluisweergave.Naam(c, Namen);
|
||||
|
||||
private static string Voorbeeld(VaultCategory c) => c switch
|
||||
{
|
||||
@@ -640,6 +739,19 @@ else
|
||||
public class NieuwItem
|
||||
{
|
||||
public VaultCategory Categorie { get; set; } = VaultCategory.Uitvaartwens;
|
||||
|
||||
/// <summary>
|
||||
/// Dezelfde keuze als nummer. De keuzelijst bindt hieraan en niet aan de enum: een soort
|
||||
/// die de beheerder maakte heeft geen naam in de enum, alleen een nummer.
|
||||
/// </summary>
|
||||
public int CategorieNummer
|
||||
{
|
||||
get => (int)Categorie;
|
||||
set => Categorie = (VaultCategory)value;
|
||||
}
|
||||
|
||||
/// <summary>De ingevulde velden van een soort die de beheerder zelf maakte, op sleutel.</summary>
|
||||
public Dictionary<string, string> Velden { get; set; } = new();
|
||||
public string Titel { get; set; } = "";
|
||||
public string? Notitie { get; set; }
|
||||
public string? Website { get; set; }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@inject ILegacyRepository LegacyOpslag
|
||||
@inject IPersonRepository Personen
|
||||
@inject VaultService Kluis
|
||||
@inject VaultCategoryService SoortService
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Nalatenschap - MijnBesluit</PageTitle>
|
||||
@@ -126,7 +127,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
|
||||
<div class="zacht">@Kluisweergave.Naam(item.Category, Namen)</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
||||
</div>
|
||||
@@ -155,7 +156,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
||||
<div class="rij">
|
||||
<div class="groei">
|
||||
<span>@item.Title</span>
|
||||
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
|
||||
<div class="zacht">@Kluisweergave.Naam(item.Category, Namen)</div>
|
||||
</div>
|
||||
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
||||
</div>
|
||||
@@ -173,7 +174,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
||||
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
||||
<div class="stapel">
|
||||
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud))
|
||||
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud, GeopendSoort))
|
||||
{
|
||||
<div class="veld">
|
||||
<label>@label</label>
|
||||
@@ -388,6 +389,12 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
||||
private IReadOnlyList<VaultItem>? Nagelaten;
|
||||
private VaultPayload? Inhoud;
|
||||
private string? GeopendTitel;
|
||||
|
||||
/// <summary>Namen van alle soorten op nummer, ook die de beheerder zelf maakte.</summary>
|
||||
private IReadOnlyDictionary<int, string> Namen = new Dictionary<int, string>();
|
||||
|
||||
/// <summary>De soort van het geopende item, voor de namen van zijn eigen velden.</summary>
|
||||
private KluisSoort? GeopendSoort;
|
||||
private IReadOnlyList<VaultFile>? GeopendBijlagen;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -397,6 +404,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
||||
_gebruikerId = user.UserId() ?? 0;
|
||||
_persoonId = user.PersonId() ?? 0;
|
||||
|
||||
Namen = await SoortService.NamenAsync();
|
||||
_familieId = (await FamilieOpslag.ListOwnedByPersonAsync(_persoonId)).FirstOrDefault()?.Id ?? 0;
|
||||
if (_familieId != 0) Leden = await Families.ListMembersAsync(_familieId, _gebruikerId);
|
||||
|
||||
@@ -465,6 +473,9 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
||||
Inhoud = resultaat.Value;
|
||||
GeopendTitel = item.Title;
|
||||
|
||||
// De velden van het item van een ander: de namen zijn geen geheim, de waarden blijven versleuteld.
|
||||
GeopendSoort = await SoortService.SoortAsync((int)item.Category);
|
||||
|
||||
// Bijlagen zijn optioneel: als ophalen mislukt (bv. geen toegang) tonen we het
|
||||
// item gewoon zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
|
||||
var bijlagen = await Kluis.ListFilesAsync(item.Id, _gebruikerId);
|
||||
@@ -476,6 +487,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
||||
GeopendItem = null;
|
||||
Inhoud = null;
|
||||
GeopendTitel = null;
|
||||
GeopendSoort = null;
|
||||
GeopendBijlagen = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public static class ClientChecks
|
||||
Accounts = Wereld.Accounts,
|
||||
Families = Wereld.Families,
|
||||
Vault = Wereld.Vault,
|
||||
Soorten = Wereld.Soorten,
|
||||
Messages = Wereld.Messages,
|
||||
Legacy = Wereld.Legacy,
|
||||
Overzicht = Wereld.Overzicht,
|
||||
@@ -64,6 +65,67 @@ public static class ClientChecks
|
||||
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.TestAsync("App: de soortenlijst komt over de lijn en de ingebouwde lijst in de app klopt ermee", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var lijst = await app.KluisSoortenAsync();
|
||||
Assert.True(lijst.Gelukt, $"de lijst komt binnen ({lijst.Melding})");
|
||||
|
||||
var server = lijst.Waarde!;
|
||||
var ingebouwd = IngebouwdeSoorten.Lijst;
|
||||
Assert.True(server.Select(s => (s.Id, s.Naam, s.BesprekenMet)).SequenceEqual(
|
||||
ingebouwd.Select(s => (s.Id, s.Naam, s.BesprekenMet))),
|
||||
"de ingebouwde lijst in de app is gelijk aan wat de server als startlijst heeft");
|
||||
Assert.True(IngebouwdeSoorten.Naam(11) == "Paspoort" && IngebouwdeSoorten.Naam(4242) == "Overig",
|
||||
"een onbekend nummer heet Overig");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een eigen soort met velden, en de naam reist mee met het item", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
var w = proef.Wereld;
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Beheer");
|
||||
w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true;
|
||||
var soortId = (await w.Soorten.MaakAsync(beheerder.Id, "Niet reanimeren", "Alleen een papieren verklaring is leidend.", false)).Value;
|
||||
await w.Soorten.VeldToevoegenAsync(beheerder.Id, soortId, "Wil je gereanimeerd worden?", "keuze",
|
||||
"Ja\nNee\nAlleen na overleg", "Kies wat het beste past.", true);
|
||||
await w.Soorten.VeldToevoegenAsync(beheerder.Id, soortId, "Datum verklaring", "datum", null, null, false);
|
||||
|
||||
var lijst = (await app.KluisSoortenAsync()).Waarde!;
|
||||
var eigen = lijst.Single(s => s.Id == soortId);
|
||||
Assert.True(!eigen.Ingebouwd && eigen.Uitleg!.Contains("papieren"), "de soort komt met zijn uitleg binnen");
|
||||
Assert.True(eigen.Velden.Count == 2, "met twee velden");
|
||||
Assert.True(eigen.Velden[0].Soort == KluisVeld.Keuze && eigen.Velden[0].Verplicht
|
||||
&& eigen.Velden[0].Opties.SequenceEqual(new[] { "Ja", "Nee", "Alleen na overleg" }),
|
||||
"het keuzeveld heeft zijn opties en is verplicht");
|
||||
Assert.True(eigen.Velden[1].Soort == KluisVeld.Datum && !eigen.Velden[1].Verplicht, "het datumveld is niet verplicht");
|
||||
|
||||
var inhoud = new KluisInhoud
|
||||
{
|
||||
Velden = new() { ["wil_je_gereanimeerd_worden"] = "Nee", ["datum_verklaring"] = "2026-10-12" }
|
||||
};
|
||||
var opgeslagen = await app.KluisOpslaanAsync(soortId, "Mijn verklaring", inhoud, 0);
|
||||
Assert.True(opgeslagen.Gelukt, $"het item wordt bewaard ({opgeslagen.Melding})");
|
||||
|
||||
var items = (await app.KluisAsync()).Waarde!;
|
||||
var item = items.Single(i => i.Title == "Mijn verklaring");
|
||||
Assert.True(item.Category == soortId, "het item staat in de nieuwe soort");
|
||||
Assert.True(item.CategoryName == "Niet reanimeren", "en de naam reist met het item mee");
|
||||
Assert.True(item.SoortNaam(IngebouwdeSoorten.Naam) == "Niet reanimeren", "ook zonder de lijst van de app");
|
||||
|
||||
var open = (await app.KluisOpenenAsync(item.Id)).Waarde!;
|
||||
Assert.True(open.Velden["wil_je_gereanimeerd_worden"] == "Nee" && open.Velden["datum_verklaring"] == "2026-10-12",
|
||||
"de velden komen er precies zo uit als ze erin gingen");
|
||||
|
||||
// Een ingebouwd item krijgt zijn ingebouwde naam, ook zonder CategoryName van de server.
|
||||
var polis = new KluisItem(1, "Polis", 2, 0, null, DateTime.UtcNow);
|
||||
Assert.True(polis.SoortNaam(IngebouwdeSoorten.Naam) == "Polis", "terugval op de ingebouwde naam");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: registreren met een code en de sessie bewaren", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
|
||||
@@ -25,6 +25,8 @@ public class Store
|
||||
public List<Adverteerder> Adverteerders = new();
|
||||
public List<Campagne> Campagnes = new();
|
||||
public List<CampagneUur> CampagneUren = new();
|
||||
public List<VaultCategoryDefinition> Soorten = VaultCategorySaatgoed.Maak();
|
||||
public List<VaultCategoryField> SoortVelden = new();
|
||||
|
||||
private long _seq;
|
||||
public long NextId() => ++_seq;
|
||||
@@ -528,3 +530,66 @@ public class TestPush : IPushSender
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>De startrijen uit db/21_kluissoorten.sql, zodat de tests met dezelfde soorten beginnen als de echte database.</summary>
|
||||
public static class VaultCategorySaatgoed
|
||||
{
|
||||
public static List<VaultCategoryDefinition> Maak()
|
||||
{
|
||||
VaultCategoryDefinition S(int id, string naam, int volgorde, bool bespreken = false) => new()
|
||||
{
|
||||
Id = id, Naam = naam, Actief = true, Volgorde = volgorde, BesprekenMet = bespreken, Ingebouwd = true
|
||||
};
|
||||
|
||||
return new List<VaultCategoryDefinition>
|
||||
{
|
||||
S(3, "Wachtwoord", 10), S(8, "Sociale media", 20), S(4, "Apparaat", 30), S(5, "Pincode", 40),
|
||||
S(2, "Polis", 50), S(1, "Uitvaartwens", 60, true), S(9, "Donorcodicil", 70, true),
|
||||
S(10, "Rijbewijs", 80), S(11, "Paspoort", 90), S(6, "Document", 100), S(7, "Notitie", 110),
|
||||
S(99, "Overig", 120)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class VaultCategoryRepo : IVaultCategoryRepository
|
||||
{
|
||||
private readonly Store _s; public VaultCategoryRepo(Store s) => _s = s;
|
||||
|
||||
private VaultCategoryDefinition Met(VaultCategoryDefinition d)
|
||||
{
|
||||
d.Velden = _s.SoortVelden.Where(v => v.CategoryId == d.Id).ToList();
|
||||
return d;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<VaultCategoryDefinition>> ListAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<VaultCategoryDefinition>>(
|
||||
_s.Soorten.OrderBy(c => c.Volgorde).ThenBy(c => c.Naam).Select(Met).ToList());
|
||||
|
||||
public Task<VaultCategoryDefinition?> FindAsync(int id, CancellationToken ct = default) =>
|
||||
Task.FromResult(_s.Soorten.Where(c => c.Id == id).Select(Met).FirstOrDefault());
|
||||
|
||||
public Task<VaultCategoryField?> FindFieldAsync(long fieldId, CancellationToken ct = default) =>
|
||||
Task.FromResult(_s.SoortVelden.FirstOrDefault(f => f.Id == fieldId));
|
||||
|
||||
public Task<IReadOnlyList<MijnBesluit.Application.Models.SoortGebruik>> GebruikAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<MijnBesluit.Application.Models.SoortGebruik>>(
|
||||
_s.VaultItems.Where(v => v.DeletedAt == null).GroupBy(v => v.Category)
|
||||
.Select(g => new MijnBesluit.Application.Models.SoortGebruik(
|
||||
(int)g.Key, g.Count(), g.Select(v => v.OwnerPersonId).Distinct().Count())).ToList());
|
||||
|
||||
public Task<IReadOnlySet<int>> InGebruikDoorAsync(long personId, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlySet<int>>(
|
||||
_s.VaultItems.Where(v => v.OwnerPersonId == personId && v.DeletedAt == null)
|
||||
.Select(v => (int)v.Category).ToHashSet());
|
||||
|
||||
public Task<int> AantalItemsAsync(int categoryId, CancellationToken ct = default) =>
|
||||
Task.FromResult(_s.VaultItems.Count(v => (int)v.Category == categoryId && v.DeletedAt == null));
|
||||
|
||||
private int _volgende = VaultCategoryDefinition.EersteEigenNummer;
|
||||
|
||||
public void Add(VaultCategoryDefinition soort) { if (soort.Id == 0) soort.Id = _volgende++; _s.Soorten.Add(soort); }
|
||||
public void Remove(VaultCategoryDefinition soort) { _s.Soorten.Remove(soort); _s.SoortVelden.RemoveAll(v => v.CategoryId == soort.Id); }
|
||||
public void Add(VaultCategoryField veld) { if (veld.Id == 0) veld.Id = _s.NextId(); _s.SoortVelden.Add(veld); }
|
||||
public void Remove(VaultCategoryField veld) => _s.SoortVelden.Remove(veld);
|
||||
}
|
||||
|
||||
@@ -1942,5 +1942,6 @@ ClientChecks.Register(runner);
|
||||
ServiceChecks.Register(runner);
|
||||
AdvertentieChecks.Register(runner);
|
||||
UitrolChecks.Register(runner);
|
||||
SoortenChecks.Register(runner);
|
||||
|
||||
return runner.Summary();
|
||||
|
||||
@@ -25,6 +25,7 @@ public class World
|
||||
public BeheerService Beheer { get; }
|
||||
public AdvertentieService Adverteren { get; }
|
||||
public AccountOpheffenService Opheffen { get; }
|
||||
public VaultCategoryService Soorten { 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 = "")
|
||||
@@ -106,7 +107,8 @@ public class World
|
||||
Accounts = new AccountService(users, people, families, codes, keys, Email, audit, uow, Clock, vault);
|
||||
Families = new FamilyService(families, people, users, invitations, messages, legacy,
|
||||
Email, audit, uow, Clock);
|
||||
Vault = new VaultService(vault, people, messages, users, keys, files, audit, uow, Clock, Verkleiner, families, legacy);
|
||||
Soorten = new VaultCategoryService(new VaultCategoryRepo(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);
|
||||
Legacy = new LegacyService(legacy, people, users, vault, messages, Email, audit, uow, Clock, Vault, families);
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
using MijnBesluit.Application.Models;
|
||||
using MijnBesluit.Application.Services;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>
|
||||
/// De soorten kluisitems uit de database: wat de beheerder kan instellen en welke regels daarbij
|
||||
/// vastliggen.
|
||||
///
|
||||
/// Het hart is één afspraak: wat in gebruik is, verdwijnt nooit. Een soort waar een item mee is
|
||||
/// gemaakt kan niet worden verwijderd, en niet-actief zetten haalt hem alleen weg bij wie hem nog
|
||||
/// nooit gebruikte. De rest hier is daar de omlijsting van: wie het mag instellen, hoe de velden
|
||||
/// van een eigen soort worden gecontroleerd, en dat "Bespreken met" een instelling per soort is.
|
||||
/// </summary>
|
||||
public static class SoortenChecks
|
||||
{
|
||||
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, string naam, bool bespreken = false)
|
||||
{
|
||||
var r = await w.Soorten.MaakAsync(beheerder.Id, naam, null, bespreken);
|
||||
if (!r.Succeeded) throw new AssertionException($"soort maken mislukte: {r.Error}");
|
||||
return r.Value;
|
||||
}
|
||||
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.TestAsync("Soorten: de startlijst is precies wat er voorheen vast in de code stond", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
|
||||
var lijst = await w.Soorten.ZichtbaarVoorAsync(jan.PersonId);
|
||||
Assert.True(lijst.Count == 12, "twaalf soorten: elf ingebouwde en Overig");
|
||||
Assert.True(lijst.First().Naam == "Wachtwoord" && lijst.Last().Naam == "Overig",
|
||||
"de volgorde is die van de oude keuzelijst");
|
||||
|
||||
var bespreekbaar = lijst.Where(s => s.BesprekenMet).Select(s => s.Naam).OrderBy(n => n).ToArray();
|
||||
Assert.True(bespreekbaar.SequenceEqual(new[] { "Donorcodicil", "Uitvaartwens" }),
|
||||
"alleen uitvaartwens en donorcodicil kennen Bespreken met, zoals voorheen");
|
||||
Assert.True(lijst.All(s => s.Ingebouwd && s.Velden.Count == 0), "ingebouwde soorten hebben geen eigen velden");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: alleen een beheerder maakt of wijzigt soorten", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
Assert.True(!(await w.Soorten.MaakAsync(jan.Id, "Dementie", null, false)).Succeeded,
|
||||
"een gewone gebruiker maakt geen soort");
|
||||
Assert.True(!(await w.Soorten.OverzichtAsync(jan.Id)).Succeeded, "en ziet het beheeroverzicht niet");
|
||||
Assert.True(!(await w.Soorten.WijzigAsync(jan.Id, 3, "Wachtwoord", null, false, 10, false)).Succeeded,
|
||||
"en zet geen soort uit");
|
||||
|
||||
var id = await MaakAsync(w, beheer, "Dementie");
|
||||
Assert.True(id >= VaultCategoryDefinition.EersteEigenNummer, "een eigen soort begint bij 100");
|
||||
Assert.True(!(await w.Soorten.VerwijderAsync(jan.Id, id)).Succeeded, "een gewone gebruiker verwijdert er geen");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: een naam is verplicht en kan niet dubbel", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
Assert.True(!(await w.Soorten.MaakAsync(beheer.Id, " ", null, false)).Succeeded, "zonder naam kan het niet");
|
||||
Assert.True(!(await w.Soorten.MaakAsync(beheer.Id, "polis", null, false)).Succeeded,
|
||||
"Polis bestaat al, ook in kleine letters");
|
||||
await MaakAsync(w, beheer, "Dementie");
|
||||
Assert.True(!(await w.Soorten.MaakAsync(beheer.Id, "DEMENTIE", null, false)).Succeeded, "een tweede Dementie kan niet");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: een nieuwe soort staat meteen in de lijst van iedereen", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var id = await MaakAsync(w, beheer, "Niet reanimeren");
|
||||
var lijst = await w.Soorten.ZichtbaarVoorAsync(jan.PersonId);
|
||||
Assert.True(lijst.Any(s => s.Id == id && s.Naam == "Niet reanimeren" && !s.Ingebouwd),
|
||||
"de nieuwe soort staat erbij");
|
||||
Assert.True(lijst.Last().Naam == "Niet reanimeren", "hij staat onderaan");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: het beheeroverzicht telt items en eigenaren, niet meer", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var piet = await w.MaakGebruikerAsync("piet@example.nl", "Piet");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
await w.Vault.CreateAsync(jan.Id, VaultCategory.Polis, "A", new VaultPayload());
|
||||
await w.Vault.CreateAsync(jan.Id, VaultCategory.Polis, "B", new VaultPayload());
|
||||
await w.Vault.CreateAsync(piet.Id, VaultCategory.Polis, "C", new VaultPayload());
|
||||
var weg = (await w.Vault.CreateAsync(piet.Id, VaultCategory.Polis, "D", new VaultPayload())).Value!;
|
||||
await w.Vault.DeleteAsync(weg.Id, piet.Id);
|
||||
|
||||
var overzicht = (await w.Soorten.OverzichtAsync(beheer.Id)).Value!;
|
||||
var polis = overzicht.Single(s => s.Naam == "Polis");
|
||||
Assert.True(polis.Items == 3, "drie items: het verwijderde telt niet mee");
|
||||
Assert.True(polis.Eigenaren == 2, "van twee mensen");
|
||||
Assert.True(!polis.KanVerwijderen, "een ingebouwde soort in gebruik is niet te verwijderen");
|
||||
Assert.True(overzicht.Single(s => s.Naam == "Paspoort").Items == 0, "wat niemand gebruikt staat op nul");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: wat in gebruik is kan niet worden verwijderd, wat ongebruikt is wel", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var gebruikt = await MaakAsync(w, beheer, "Dementie");
|
||||
var ongebruikt = await MaakAsync(w, beheer, "Proef");
|
||||
|
||||
var item = (await w.Vault.CreateAsync(jan.Id, (VaultCategory)gebruikt, "Mijn wens",
|
||||
new VaultPayload { Velden = new() })).Value!;
|
||||
Assert.True(item.Id != 0, "Jan maakt een item in de nieuwe soort");
|
||||
|
||||
var geweigerd = await w.Soorten.VerwijderAsync(beheer.Id, gebruikt);
|
||||
Assert.True(!geweigerd.Succeeded && geweigerd.Error!.Contains("niet-actief"),
|
||||
"een soort in gebruik wordt geweigerd, met de uitleg wat wel kan");
|
||||
Assert.True(await w.Soorten.VindAsync(gebruikt) is not null, "hij staat er nog");
|
||||
|
||||
Assert.True((await w.Soorten.VerwijderAsync(beheer.Id, ongebruikt)).Succeeded, "een ongebruikte kan weg");
|
||||
Assert.True(await w.Soorten.VindAsync(ongebruikt) is null, "en is weg");
|
||||
|
||||
Assert.True(!(await w.Soorten.VerwijderAsync(beheer.Id, 3)).Succeeded,
|
||||
"een ingebouwde soort verwijder je nooit, ook niet zonder items");
|
||||
|
||||
// En zodra het laatste item weg is, mag het weer.
|
||||
await w.Vault.DeleteAsync(item.Id, jan.Id);
|
||||
Assert.True((await w.Soorten.VerwijderAsync(beheer.Id, gebruikt)).Succeeded,
|
||||
"zonder items is hij weer te verwijderen");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: niet-actief verdwijnt alleen bij wie hem nooit gebruikte", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var piet = await w.MaakGebruikerAsync("piet@example.nl", "Piet");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var id = await MaakAsync(w, beheer, "Dementie");
|
||||
var item = (await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "Mijn wens",
|
||||
new VaultPayload { Velden = new() })).Value!;
|
||||
|
||||
Assert.True((await w.Soorten.WijzigAsync(beheer.Id, id, "Dementie", null, false, 1000, false)).Succeeded,
|
||||
"de beheerder zet hem op niet-actief");
|
||||
|
||||
Assert.True((await w.Soorten.ZichtbaarVoorAsync(jan.PersonId)).Any(s => s.Id == id),
|
||||
"Jan heeft er een item van en blijft de soort zien");
|
||||
Assert.True(!(await w.Soorten.ZichtbaarVoorAsync(piet.PersonId)).Any(s => s.Id == id),
|
||||
"Piet heeft hem nooit gebruikt en ziet hem niet meer");
|
||||
|
||||
Assert.True(!(await w.Vault.CreateAsync(piet.Id, (VaultCategory)id, "Nieuw", new VaultPayload { Velden = new() })).Succeeded,
|
||||
"Piet kan er ook geen nieuw item meer in maken");
|
||||
|
||||
var tweede = await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "Nog een", new VaultPayload { Velden = new() });
|
||||
Assert.True(tweede.Succeeded, "Jan kan er nog wel nieuwe items in maken: het is een soort die hij kent");
|
||||
|
||||
var wijzigen = await w.Vault.UpdateAsync(item.Id, jan.Id, (VaultCategory)id, "Mijn wens (nieuw)",
|
||||
new VaultPayload { Velden = new() }, ReleasePolicy.AlleenIkzelf);
|
||||
Assert.True(wijzigen.Succeeded, "en zijn bestaande item kan hij gewoon wijzigen");
|
||||
|
||||
Assert.True((await w.Vault.OpenAsync(item.Id, jan.Id)).Succeeded, "en openen");
|
||||
|
||||
// Weer aanzetten: iedereen ziet hem weer.
|
||||
await w.Soorten.WijzigAsync(beheer.Id, id, "Dementie", null, true, 1000, false);
|
||||
Assert.True((await w.Soorten.ZichtbaarVoorAsync(piet.PersonId)).Any(s => s.Id == id),
|
||||
"na weer aanzetten ziet Piet hem weer");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: ook een ingebouwde soort kan op niet-actief, met dezelfde regel", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var piet = await w.MaakGebruikerAsync("piet@example.nl", "Piet");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
await w.Vault.CreateAsync(jan.Id, VaultCategory.Paspoort, "Mijn paspoort", new VaultPayload());
|
||||
await w.Soorten.WijzigAsync(beheer.Id, (int)VaultCategory.Paspoort, "Paspoort", null, false, 90, false);
|
||||
|
||||
Assert.True((await w.Soorten.ZichtbaarVoorAsync(jan.PersonId)).Any(s => s.Naam == "Paspoort"),
|
||||
"Jan houdt Paspoort");
|
||||
Assert.True(!(await w.Soorten.ZichtbaarVoorAsync(piet.PersonId)).Any(s => s.Naam == "Paspoort"),
|
||||
"Piet ziet het niet meer");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: Bespreken met is een instelling per soort", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var opa = await w.MaakGebruikerAsync("opa@example.nl", "Klaas", "de Vries");
|
||||
var zoon = await w.MaakFamilielidAsync(opa, "Peter", "de Vries", "zoon@example.nl");
|
||||
await w.BeheerderAsync(opa, zoon);
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
// Standaard: een uitvaartwens kan het, een polis niet - zoals voorheen.
|
||||
var wens = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Uitvaart", new VaultPayload(),
|
||||
ReleasePolicy.AlleenIkzelf, null, zoon.PersonId)).Value!;
|
||||
Assert.True((await w.Vault.OpenAsync(wens.Id, zoon.Id)).Succeeded, "de gekozen beheerder leest de uitvaartwens mee");
|
||||
|
||||
var polis = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Polis, "Polis", new VaultPayload(),
|
||||
ReleasePolicy.AlleenIkzelf, null, zoon.PersonId)).Value!;
|
||||
Assert.True(polis.SharedManagerPersonId is null, "bij een polis wordt de keuze genegeerd");
|
||||
Assert.True(!(await w.Vault.OpenAsync(polis.Id, zoon.Id)).Succeeded, "en de beheerder ziet hem niet");
|
||||
|
||||
// De beheerder zet het bij Polis aan: nu werkt het ook daar.
|
||||
await w.Soorten.WijzigAsync(beheer.Id, (int)VaultCategory.Polis, "Polis", null, true, 50, true);
|
||||
var polis2 = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Polis, "Polis 2", new VaultPayload(),
|
||||
ReleasePolicy.AlleenIkzelf, null, zoon.PersonId)).Value!;
|
||||
Assert.True(polis2.SharedManagerPersonId == zoon.PersonId, "bij Polis wordt de keuze nu bewaard");
|
||||
Assert.True((await w.Vault.OpenAsync(polis2.Id, zoon.Id)).Succeeded, "en de beheerder leest hem mee");
|
||||
|
||||
// En bij Uitvaartwens uit: dan niet meer.
|
||||
await w.Soorten.WijzigAsync(beheer.Id, (int)VaultCategory.Uitvaartwens, "Uitvaartwens", null, true, 60, false);
|
||||
var wens2 = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Uitvaart 2", new VaultPayload(),
|
||||
ReleasePolicy.AlleenIkzelf, null, zoon.PersonId)).Value!;
|
||||
Assert.True(wens2.SharedManagerPersonId is null, "met de schakelaar uit wordt de keuze genegeerd");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: Bespreken met werkt ook bij een soort die de beheerder zelf maakte", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var opa = await w.MaakGebruikerAsync("opa@example.nl", "Klaas", "de Vries");
|
||||
var zoon = await w.MaakFamilielidAsync(opa, "Peter", "de Vries", "zoon@example.nl");
|
||||
await w.BeheerderAsync(opa, zoon);
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var met = await MaakAsync(w, beheer, "Niet reanimeren", bespreken: true);
|
||||
var zonder = await MaakAsync(w, beheer, "Dementie", bespreken: false);
|
||||
|
||||
var a = (await w.Vault.CreateAsync(opa.Id, (VaultCategory)met, "NR", new VaultPayload { Velden = new() },
|
||||
ReleasePolicy.AlleenIkzelf, null, zoon.PersonId)).Value!;
|
||||
var b = (await w.Vault.CreateAsync(opa.Id, (VaultCategory)zonder, "DM", new VaultPayload { Velden = new() },
|
||||
ReleasePolicy.AlleenIkzelf, null, zoon.PersonId)).Value!;
|
||||
Assert.True((await w.Vault.OpenAsync(a.Id, zoon.Id)).Succeeded, "met de schakelaar aan leest hij mee");
|
||||
Assert.True(!(await w.Vault.OpenAsync(b.Id, zoon.Id)).Succeeded, "met de schakelaar uit niet");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: een soort die niet bestaat is geen soort", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var r = await w.Vault.CreateAsync(jan.Id, (VaultCategory)4242, "Onzin", new VaultPayload());
|
||||
Assert.True(!r.Succeeded, "een onbekend soortnummer wordt geweigerd");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: de sleutel van een veld komt uit het label en is uniek", () =>
|
||||
{
|
||||
Assert.True(VaultCategoryService.NieuweSleutel("Wil je gereanimeerd worden?", Array.Empty<string>())
|
||||
== "wil_je_gereanimeerd_worden", "een vraag wordt een leesbare sleutel");
|
||||
Assert.True(VaultCategoryService.NieuweSleutel("Café — één", Array.Empty<string>()) == "cafe_een",
|
||||
"accenten en streepjes verdwijnen");
|
||||
Assert.True(VaultCategoryService.NieuweSleutel("???", Array.Empty<string>()) == "veld",
|
||||
"zonder bruikbare tekens blijft 'veld' over");
|
||||
Assert.True(VaultCategoryService.NieuweSleutel("Arts", new[] { "arts" }) == "arts_2",
|
||||
"een bestaande sleutel krijgt een nummer");
|
||||
Assert.True(VaultCategoryService.NieuweSleutel(new string('x', 80), Array.Empty<string>()).Length <= 30,
|
||||
"en een lang label wordt bijgesneden");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
runner.TestAsync("Velden: een eigen soort bewaart zijn velden versleuteld en geeft ze terug", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var id = await MaakAsync(w, beheer, "Niet reanimeren");
|
||||
Assert.True((await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Wil je gereanimeerd worden?", VeldSoort.Keuze,
|
||||
"Ja\nNee\nAlleen na overleg", null, true)).Succeeded, "keuzeveld toevoegen");
|
||||
Assert.True((await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Datum verklaring", VeldSoort.Datum,
|
||||
null, null, false)).Succeeded, "datumveld toevoegen");
|
||||
Assert.True((await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Toelichting voor de arts", VeldSoort.Lang,
|
||||
null, "Zo precies mogelijk.", false)).Succeeded, "lang veld toevoegen");
|
||||
|
||||
var soort = (await w.Soorten.ZichtbaarVoorAsync(jan.PersonId)).Single(s => s.Id == id);
|
||||
Assert.True(soort.Velden.Select(v => v.Sleutel).SequenceEqual(
|
||||
new[] { "wil_je_gereanimeerd_worden", "datum_verklaring", "toelichting_voor_de_arts" }),
|
||||
"de velden staan in de volgorde van toevoegen");
|
||||
Assert.True(soort.Velden[0].Opties.SequenceEqual(new[] { "Ja", "Nee", "Alleen na overleg" }), "de opties kloppen");
|
||||
|
||||
var item = await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "Mijn verklaring", new VaultPayload
|
||||
{
|
||||
Velden = new()
|
||||
{
|
||||
["wil_je_gereanimeerd_worden"] = "Nee",
|
||||
["datum_verklaring"] = "2026-10-12",
|
||||
["onbekend_veld"] = "hoort er niet"
|
||||
}
|
||||
});
|
||||
Assert.True(item.Succeeded, "het item wordt bewaard");
|
||||
|
||||
var open = (await w.Vault.OpenAsync(item.Value!.Id, jan.Id)).Value!;
|
||||
Assert.True(open.Velden is not null && open.Velden["wil_je_gereanimeerd_worden"] == "Nee", "de waarde komt terug");
|
||||
Assert.True(open.Velden!["datum_verklaring"] == "2026-10-12", "ook de datum");
|
||||
Assert.True(!open.Velden.ContainsKey("onbekend_veld"), "een veld dat niet bestaat wordt weggelaten");
|
||||
|
||||
// En het staat echt versleuteld in de opslag, niet leesbaar.
|
||||
var opgeslagen = w.Store.VaultItems.Single(v => v.Id == item.Value.Id);
|
||||
Assert.True(!System.Text.Encoding.UTF8.GetString(opgeslagen.Ciphertext).Contains("Nee"),
|
||||
"in de opslag staat niets leesbaars");
|
||||
});
|
||||
|
||||
runner.TestAsync("Velden: verplicht, datum en lengte worden op de server gecontroleerd", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var id = await MaakAsync(w, beheer, "Niet reanimeren");
|
||||
await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Keuze", VeldSoort.Keuze, "Ja\nNee", null, true);
|
||||
await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Datum", VeldSoort.Datum, null, null, false);
|
||||
|
||||
var leeg = await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "X", new VaultPayload { Velden = new() });
|
||||
Assert.True(!leeg.Succeeded && leeg.Error!.Contains("Keuze"), "een leeg verplicht veld wordt geweigerd, met de naam erbij");
|
||||
|
||||
var slechteDatum = await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "X", new VaultPayload
|
||||
{ Velden = new() { ["keuze"] = "Ja", ["datum"] = "morgen" } });
|
||||
Assert.True(!slechteDatum.Succeeded && slechteDatum.Error!.Contains("datum"), "een datum die geen datum is wordt geweigerd");
|
||||
|
||||
var teLang = await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "X", new VaultPayload
|
||||
{ Velden = new() { ["keuze"] = new string('a', VaultCategoryService.MaxWaardelengte + 1) } });
|
||||
Assert.True(!teLang.Succeeded, "een te lange waarde wordt geweigerd");
|
||||
|
||||
var goed = await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "X", new VaultPayload
|
||||
{ Velden = new() { ["keuze"] = "Ja", ["datum"] = "2026-02-28" } });
|
||||
Assert.True(goed.Succeeded, "met alles goed ingevuld lukt het");
|
||||
});
|
||||
|
||||
runner.TestAsync("Velden: een app die de soort niet kent kan geen gegevens overschrijven met niets", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var id = await MaakAsync(w, beheer, "Dementie");
|
||||
await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Wens", VeldSoort.Lang, null, null, false);
|
||||
var item = (await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "Mijn wens", new VaultPayload
|
||||
{ Velden = new() { ["wens"] = "Thuis blijven wonen." } })).Value!;
|
||||
|
||||
// Een oudere app stuurt de inhoud zonder het onderdeel Velden mee.
|
||||
var oud = await w.Vault.UpdateAsync(item.Id, jan.Id, (VaultCategory)id, "Mijn wens",
|
||||
new VaultPayload { Notes = "alleen een notitie" }, ReleasePolicy.AlleenIkzelf);
|
||||
Assert.True(!oud.Succeeded && oud.Error!.Contains("app"), "het wijzigen wordt geweigerd met het advies de app bij te werken");
|
||||
|
||||
var open = (await w.Vault.OpenAsync(item.Id, jan.Id)).Value!;
|
||||
Assert.True(open.Velden!["wens"] == "Thuis blijven wonen.", "wat er stond is niet overschreven");
|
||||
});
|
||||
|
||||
runner.TestAsync("Velden: een ingebouwde soort bewaart geen eigen velden", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
|
||||
var item = (await w.Vault.CreateAsync(jan.Id, VaultCategory.Polis, "Polis", new VaultPayload
|
||||
{ Insurer = "Zilveren Kruis", Velden = new() { ["iets"] = "weg ermee" } })).Value!;
|
||||
var open = (await w.Vault.OpenAsync(item.Id, jan.Id)).Value!;
|
||||
Assert.True(open.Velden is null, "bij een ingebouwde soort wordt Velden leeggemaakt");
|
||||
Assert.True(open.Insurer == "Zilveren Kruis", "de eigen velden van de soort blijven gewoon");
|
||||
});
|
||||
|
||||
runner.TestAsync("Velden: ingebouwde soorten hebben geen velden om te wijzigen", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var r = await w.Soorten.VeldToevoegenAsync(beheer.Id, (int)VaultCategory.Polis, "Extra", VeldSoort.Tekst, null, null, false);
|
||||
Assert.True(!r.Succeeded, "een veld toevoegen aan een ingebouwde soort kan niet");
|
||||
});
|
||||
|
||||
runner.TestAsync("Velden: een veld met ingevulde gegevens wordt niet verwijderd maar niet-actief", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
||||
var beheer = await BeheerderAsync(w);
|
||||
|
||||
var id = await MaakAsync(w, beheer, "Dementie");
|
||||
await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Wens", VeldSoort.Lang, null, null, false);
|
||||
await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Arts", VeldSoort.Tekst, null, null, false);
|
||||
var velden = (await w.Soorten.OverzichtAsync(beheer.Id)).Value!.Single(s => s.Id == id).Velden;
|
||||
var wens = velden.Single(v => v.Sleutel == "wens");
|
||||
var arts = velden.Single(v => v.Sleutel == "arts");
|
||||
|
||||
// Zonder items mag een veld gewoon weg.
|
||||
Assert.True((await w.Soorten.VeldVerwijderenAsync(beheer.Id, arts.Id)).Succeeded, "zonder items kan een veld weg");
|
||||
|
||||
await w.Vault.CreateAsync(jan.Id, (VaultCategory)id, "Mijn wens", new VaultPayload
|
||||
{ Velden = new() { ["wens"] = "Thuis blijven wonen." } });
|
||||
|
||||
var geweigerd = await w.Soorten.VeldVerwijderenAsync(beheer.Id, wens.Id);
|
||||
Assert.True(!geweigerd.Succeeded && geweigerd.Error!.Contains("niet-actief"),
|
||||
"met items erbij wordt verwijderen geweigerd, met de uitleg");
|
||||
|
||||
// Niet-actief mag. De waarde blijft bewaard en bij het wijzigen nog bruikbaar.
|
||||
Assert.True((await w.Soorten.VeldWijzigenAsync(beheer.Id, wens.Id, "Wens", null, null, false, false, 10)).Succeeded,
|
||||
"op niet-actief zetten kan");
|
||||
var item = w.Store.VaultItems.Single(v => v.Title == "Mijn wens");
|
||||
var wijzig = await w.Vault.UpdateAsync(item.Id, jan.Id, (VaultCategory)id, "Mijn wens",
|
||||
new VaultPayload { Velden = new() { ["wens"] = "Thuis blijven wonen, met hulp." } }, ReleasePolicy.AlleenIkzelf);
|
||||
Assert.True(wijzig.Succeeded, "het item is nog te wijzigen");
|
||||
var open = (await w.Vault.OpenAsync(item.Id, jan.Id)).Value!;
|
||||
Assert.True(open.Velden!["wens"] == "Thuis blijven wonen, met hulp.",
|
||||
"en de waarde van het niet-actieve veld blijft bewaard");
|
||||
});
|
||||
|
||||
runner.TestAsync("Velden: een keuzelijst heeft minstens twee opties, een uitleg heeft tekst", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var beheer = await BeheerderAsync(w);
|
||||
var id = await MaakAsync(w, beheer, "Dementie");
|
||||
|
||||
Assert.True(!(await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Keuze", VeldSoort.Keuze, "Alleen deze", null, false)).Succeeded,
|
||||
"één optie is geen keuze");
|
||||
Assert.True(!(await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Uitleg", VeldSoort.Uitleg, null, null, false)).Succeeded,
|
||||
"een uitleg zonder tekst kan niet");
|
||||
Assert.True((await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Uitleg", VeldSoort.Uitleg, null,
|
||||
"Een papieren verklaring is leidend.", true)).Succeeded, "met tekst kan het wel");
|
||||
var uitleg = (await w.Soorten.OverzichtAsync(beheer.Id)).Value!.Single(s => s.Id == id).Velden.Single();
|
||||
Assert.True(!uitleg.Verplicht, "een uitlegtekst kan nooit verplicht zijn");
|
||||
Assert.True(!(await w.Soorten.VeldToevoegenAsync(beheer.Id, id, "Raar", "zweef", null, null, false)).Succeeded,
|
||||
"een soort veld die we niet kennen wordt geweigerd");
|
||||
});
|
||||
|
||||
runner.TestAsync("Soorten: wijzigingen door de beheerder staan in het logboek", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var beheer = await BeheerderAsync(w);
|
||||
var id = await MaakAsync(w, beheer, "Dementie");
|
||||
await w.Soorten.WijzigAsync(beheer.Id, id, "Dementie", null, false, 1000, false);
|
||||
await w.Soorten.VerwijderAsync(beheer.Id, id);
|
||||
|
||||
var regels = w.Store.Audit.Where(a => a.Action == AuditAction.KluisSoortBeheerd).ToList();
|
||||
Assert.True(regels.Count == 3, "gemaakt, gewijzigd en verwijderd staan er alle drie in");
|
||||
Assert.True(regels.All(a => a.UserId == beheer.Id), "met de beheerder erbij");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<#
|
||||
Bouwt de Android-bundel (AAB) voor de Play Store, voor SideKick ("Bouwen en uitrollen",
|
||||
onderdeel mijnbesluit-aab, Sleutel android-aab).
|
||||
|
||||
Zelfde werkwijze als Bouwen-android.ps1, maar met Apk-maken.ps1 -Aab. Het resultaat is een
|
||||
nl.mijnbesluit.app-Signed.aab in de map die SideKick meegeeft (-Uitvoer). Een AAB kun je niet
|
||||
op een telefoon zetten; hij gaat naar de Play Console.
|
||||
|
||||
Het wachtwoord van het sleutelbestand komt uit de gebruikersvariabele
|
||||
MIJNBESLUIT_KEYSTORE_WACHTWOORD (zie Apk-maken.ps1).
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Configuratie = 'Release',
|
||||
|
||||
# Waar de AAB terechtkomt (SideKick: <publicatiemap>\android-aab).
|
||||
[string] $Uitvoer
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($env:RENH_UITROLVENSTER -and -not $env:MIJNBESLUIT_UITROLVENSTER) {
|
||||
$env:MIJNBESLUIT_UITROLVENSTER = '1'
|
||||
}
|
||||
|
||||
$params = @{ Aab = $true }
|
||||
if ($Uitvoer) { $params['Uit'] = $Uitvoer }
|
||||
|
||||
& (Join-Path $PSScriptRoot 'Apk-maken.ps1') @params
|
||||
if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { exit $LASTEXITCODE }
|
||||
@@ -0,0 +1,36 @@
|
||||
<#
|
||||
Bouwt de Android-app voor SideKick ("Bouwen en uitrollen", onderdeel mijnbesluit-app, Sleutel android).
|
||||
|
||||
SideKick zoekt voor een onderdeel eerst tools\Bouwen-<Sleutel>.ps1. Dit script is die bouwstap:
|
||||
het geeft het werk aan Apk-maken.ps1 (dotnet publish -f net10.0-android, ondertekend met het
|
||||
sleutelbestand) en zet de APK in de map die SideKick meegeeft (-Uitvoer).
|
||||
|
||||
Zonder dit script bouwt SideKick een gewoon Windows-programma (win-x64), en dat kan een
|
||||
Android-project niet: "Unable to find package Microsoft.NETCore.App.Runtime.Mono.win-x64".
|
||||
|
||||
Het wachtwoord van het sleutelbestand komt uit de gebruikersvariabele MIJNBESLUIT_KEYSTORE_WACHTWOORD
|
||||
(zie Apk-maken.ps1). Start SideKick opnieuw als je die net hebt gezet.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# SideKick geeft dit altijd mee; een Android-release is altijd Release.
|
||||
[string] $Configuratie = 'Release',
|
||||
|
||||
# Waar de APK terechtkomt (SideKick: <publicatiemap>\android).
|
||||
[string] $Uitvoer
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# SideKick zet RENH_UITROLVENSTER; Apk-maken.ps1 kent MIJNBESLUIT_UITROLVENSTER. Zo stopt het script
|
||||
# met een duidelijke melding als het wachtwoord ontbreekt, in plaats van te blijven wachten op invoer.
|
||||
if ($env:RENH_UITROLVENSTER -and -not $env:MIJNBESLUIT_UITROLVENSTER) {
|
||||
$env:MIJNBESLUIT_UITROLVENSTER = '1'
|
||||
}
|
||||
|
||||
$params = @{}
|
||||
if ($Uitvoer) { $params['Uit'] = $Uitvoer }
|
||||
|
||||
& (Join-Path $PSScriptRoot 'Apk-maken.ps1') @params
|
||||
if ($LASTEXITCODE -ne 0 -and $null -ne $LASTEXITCODE) { exit $LASTEXITCODE }
|
||||
Reference in New Issue
Block a user