Flexibele kluissoorten: soorten in de database, beheer op de website
- Tabellen vault_categories en vault_category_fields (db/21), startrijen handmatig (db/handmatig/21b) - VaultCategoryService met regels voor zichtbaarheid, gebruik en eigen velden - Beheer > Kluissoorten (/beheer/soorten) met gebruikstellers; soort in gebruik niet te verwijderen - Kluis en Nalatenschap (web en app) tonen soorten en eigen velden uit de database - Socketprotocol: vault.categories; GRANTs voor mijnbesluit_app in db/02 - db/20 herhaalbaar gemaakt - App versie 1.1 (versiecode 3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014MGLqt1uKfNARQHut9ifcx
This commit is contained in:
@@ -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`.`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`.`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`.`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.
|
-- Het logboek mag er alleen bij komen en uit gelezen worden, niet uit verdwijnen.
|
||||||
GRANT SELECT, INSERT ON `mijnbesluit`.`audit_log` TO 'mijnbesluit_app'@'localhost';
|
GRANT SELECT, INSERT ON `mijnbesluit`.`audit_log` TO 'mijnbesluit_app'@'localhost';
|
||||||
|
|||||||
@@ -3,6 +3,33 @@
|
|||||||
-- Wie een bezorgd bericht "verwijdert" haalt het alleen bij zichzelf weg; de ander heeft er nog
|
-- 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
|
-- 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.
|
-- 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`,
|
-- Draaien met:
|
||||||
ADD COLUMN `hidden_by_recipient` TINYINT(1) NOT NULL DEFAULT 0 AFTER `hidden_by_sender`;
|
-- 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`;
|
||||||
@@ -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:
|
Bewust leesbaar, omdat er anders niet te zoeken of te sorteren valt:
|
||||||
|
|
||||||
- de titel en de categorie van een kluisitem;
|
- 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;
|
- het onderwerp, de afzender, de ontvanger en de bezorgdatum van een bericht;
|
||||||
- namen, e-mailadressen en relaties.
|
- 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).
|
||||||
@@ -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.list` | ja | Titels en categorieën (geen inhoud) |
|
||||||
| `vault.open` | ja | Eén item ontsleutelen |
|
| `vault.open` | ja | Eén item ontsleutelen |
|
||||||
| `vault.save` | ja | Item versleuteld opslaan |
|
| `vault.save` | ja | Item versleuteld opslaan |
|
||||||
|
| `vault.categories` | ja | De soorten kluisitems die jij te zien krijgt: de actieve plus die waar je al een item van hebt, met naam, uitleg, "bespreken met" en bij een eigen soort de velden van het formulier |
|
||||||
|
| `vault.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.inbox` / `message.outbox` | ja | Berichten voor en door jou |
|
||||||
| `message.read` | ja | Bericht lezen en als gelezen markeren |
|
| `message.read` | ja | Bericht lezen en als gelezen markeren |
|
||||||
| `message.compose` | ja | Bericht klaarzetten |
|
| `message.compose` | ja | Bericht klaarzetten |
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
|
|
||||||
<ApplicationTitle>MijnBesluit</ApplicationTitle>
|
<ApplicationTitle>MijnBesluit</ApplicationTitle>
|
||||||
<ApplicationId>nl.mijnbesluit.app</ApplicationId>
|
<ApplicationId>nl.mijnbesluit.app</ApplicationId>
|
||||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>1.1</ApplicationDisplayVersion>
|
||||||
<ApplicationVersion>2</ApplicationVersion>
|
<ApplicationVersion>3</ApplicationVersion>
|
||||||
|
|
||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
|
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
|
||||||
|
|
||||||
|
|||||||
@@ -140,22 +140,11 @@ public static class Tonen
|
|||||||
public static string Kort(DateOnly? dag) =>
|
public static string Kort(DateOnly? dag) =>
|
||||||
dag is null ? "" : $"{dag.Value.Day} {KorteMaanden[dag.Value.Month - 1]} {dag.Value.Year}";
|
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>
|
/// <summary>
|
||||||
public static string Categorie(int categorie) => categorie switch
|
/// 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"/>).
|
||||||
1 => "Uitvaartwens",
|
/// </summary>
|
||||||
2 => "Polis",
|
public static string Categorie(int categorie) => MijnBesluit.Client.IngebouwdeSoorten.Naam(categorie);
|
||||||
3 => "Wachtwoord",
|
|
||||||
4 => "Apparaat",
|
|
||||||
5 => "Pincode",
|
|
||||||
6 => "Document",
|
|
||||||
7 => "Notitie",
|
|
||||||
8 => "Sociale media",
|
|
||||||
9 => "Donorcodicil",
|
|
||||||
10 => "Rijbewijs",
|
|
||||||
11 => "Paspoort",
|
|
||||||
_ => "Overig"
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hoe een relatie op het scherm heet. De lijst staat in MijnBesluit.Client, zodat de app en
|
/// 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">
|
<div class="rij kluiskop">
|
||||||
<select class="groei" aria-label="Filter op soort" disabled="@Bezig" @bind="Filter" @bind:after="LaadAsync">
|
<select class="groei" aria-label="Filter op soort" disabled="@Bezig" @bind="Filter" @bind:after="LaadAsync">
|
||||||
<option value="">Alles</option>
|
<option value="">Alles</option>
|
||||||
<option value="3">Wachtwoord</option>
|
@foreach (var soort in Soorten)
|
||||||
<option value="8">Sociale media</option>
|
{
|
||||||
<option value="4">Apparaat</option>
|
<option value="@soort.Id">@soort.Naam</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>
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button class="knop" disabled="@Bezig" @onclick="() => Nieuw()">Toevoegen</button>
|
<button class="knop" disabled="@Bezig" @onclick="() => Nieuw()">Toevoegen</button>
|
||||||
@@ -52,22 +44,19 @@
|
|||||||
|
|
||||||
<div class="veld">
|
<div class="veld">
|
||||||
<label for="soort">Wat leg je vast?</label>
|
<label for="soort">Wat leg je vast?</label>
|
||||||
@* De nummers komen overeen met VaultCategory in het domein. Overig is 99, niet 9:
|
@* De lijst komt van de server: de soorten die de beheerder aanbiedt, plus de soorten
|
||||||
dat stond hier eerder verkeerd en leverde een categorie op die nergens bij hoort. *@
|
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">
|
<select id="soort" @bind="Categorie">
|
||||||
<option value="3">Wachtwoord</option>
|
@foreach (var soort in Soorten)
|
||||||
<option value="8">Sociale media</option>
|
{
|
||||||
<option value="4">Apparaat</option>
|
<option value="@soort.Id">@soort.Naam</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>
|
|
||||||
</select>
|
</select>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(HuidigeSoort?.Uitleg))
|
||||||
|
{
|
||||||
|
<div class="uitleg">@HuidigeSoort!.Uitleg</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="veld">
|
<div class="veld">
|
||||||
@@ -153,7 +142,17 @@
|
|||||||
break;
|
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">
|
<div class="veld">
|
||||||
<label for="bespreken">Bespreken met</label>
|
<label for="bespreken">Bespreken met</label>
|
||||||
@@ -278,7 +277,7 @@
|
|||||||
@onkeydown="e => ToetsAsync(e, () => TegelOpenenAsync(item))">
|
@onkeydown="e => ToetsAsync(e, () => TegelOpenenAsync(item))">
|
||||||
<div class="rij">
|
<div class="rij">
|
||||||
<div class="groei">
|
<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>
|
<div class="zacht" title="@item.Title">@item.Title</div>
|
||||||
|
|
||||||
@* stopPropagation, want anders opent een druk op de prullenbak ook meteen
|
@* 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>
|
/// <summary>Op welk soort de lijst gefilterd staat; leeg is alles. Zelfde opzet als op de website.</summary>
|
||||||
private string Filter = "";
|
private string Filter = "";
|
||||||
|
|
||||||
|
/// <summary>De soorten in de keuzelijst; zie <see cref="LaadAsync"/>.</summary>
|
||||||
|
private IReadOnlyList<KluisSoort> Soorten = IngebouwdeSoorten.Lijst;
|
||||||
|
|
||||||
private int Categorie = 3;
|
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 IReadOnlyList<Beheerder> Beheerders = Array.Empty<Beheerder>();
|
||||||
private string BesprekenMetTekst = "";
|
private string BesprekenMetTekst = "";
|
||||||
private long? BesprekenMetPersonId => long.TryParse(BesprekenMetTekst, out var p) ? p : null;
|
private long? BesprekenMetPersonId => long.TryParse(BesprekenMetTekst, out var p) ? p : null;
|
||||||
@@ -383,6 +391,11 @@
|
|||||||
if (lijst.Gelukt) Items = lijst.Waarde!;
|
if (lijst.Gelukt) Items = lijst.Waarde!;
|
||||||
else Fout = lijst.Melding;
|
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();
|
var beheerders = await Toestand.Client.BeheerdersAsync();
|
||||||
if (beheerders.Gelukt) Beheerders = beheerders.Waarde!;
|
if (beheerders.Gelukt) Beheerders = beheerders.Waarde!;
|
||||||
|
|
||||||
@@ -405,7 +418,7 @@
|
|||||||
Bewerken = new KluisInhoud();
|
Bewerken = new KluisInhoud();
|
||||||
ItemId = 0;
|
ItemId = 0;
|
||||||
Titel = "";
|
Titel = "";
|
||||||
Categorie = 3;
|
Categorie = Soorten.Any(s => s.Id == 3) ? 3 : Soorten[0].Id;
|
||||||
Vrijgave = 1;
|
Vrijgave = 1;
|
||||||
VrijgaveDatum = DateTime.Today.AddYears(1);
|
VrijgaveDatum = DateTime.Today.AddYears(1);
|
||||||
BesprekenMetTekst = "";
|
BesprekenMetTekst = "";
|
||||||
@@ -456,6 +469,15 @@
|
|||||||
var datum = Vrijgave == 2 ? VrijgaveDatum : null;
|
var datum = Vrijgave == 2 ? VrijgaveDatum : null;
|
||||||
if (Vrijgave == 2 && datum is null) { Fout = "Kies een datum waarop dit vrijkomt."; return; }
|
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;
|
long nieuwItemId = 0;
|
||||||
|
|
||||||
if (await ProbeerAsync(async () =>
|
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>
|
/// <summary>
|
||||||
/// Muziek en Wensen mogen meerdere regels worden; dan is een vast aantal regels niet
|
/// 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
|
/// 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="rij">
|
||||||
<div class="groei">
|
<div class="groei">
|
||||||
<span>@item.Title</span>
|
<span>@item.Title</span>
|
||||||
<div class="zacht">@Tonen.Categorie(item.Category)</div>
|
<div class="zacht">@item.SoortNaam(IngebouwdeSoorten.Naam)</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="knop rustig klein" disabled="@Bezig" @onclick:stopPropagation="true"
|
<button class="knop rustig klein" disabled="@Bezig" @onclick:stopPropagation="true"
|
||||||
@onclick="() => ItemOpenenAsync(item)">Openen</button>
|
@onclick="() => ItemOpenenAsync(item)">Openen</button>
|
||||||
@@ -187,7 +187,7 @@ else if (Stand is { IsBevestigd: true })
|
|||||||
<div class="rij">
|
<div class="rij">
|
||||||
<div class="groei">
|
<div class="groei">
|
||||||
<span>@item.Title</span>
|
<span>@item.Title</span>
|
||||||
<div class="zacht">@Tonen.Categorie(item.Category)</div>
|
<div class="zacht">@item.SoortNaam(IngebouwdeSoorten.Naam)</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="knop rustig klein" disabled="@Bezig" @onclick="() => ItemOpenenAsync(item)">Openen</button>
|
<button class="knop rustig klein" disabled="@Bezig" @onclick="() => ItemOpenenAsync(item)">Openen</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,7 +205,7 @@ else if (Stand is { IsBevestigd: true })
|
|||||||
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||||
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
||||||
<div class="stapel">
|
<div class="stapel">
|
||||||
@foreach (var regel in Regels(Inhoud))
|
@foreach (var regel in Regels(Inhoud).Concat(Inhoud.EigenRegels(GeopendSoort)))
|
||||||
{
|
{
|
||||||
<div class="veld">
|
<div class="veld">
|
||||||
<label>@regel.Naam</label>
|
<label>@regel.Naam</label>
|
||||||
@@ -482,6 +482,9 @@ else if (Stand is { IsBevestigd: true })
|
|||||||
private long? Bekeken, GeopendItem;
|
private long? Bekeken, GeopendItem;
|
||||||
private IReadOnlyList<KluisItem> Nagelaten = Array.Empty<KluisItem>();
|
private IReadOnlyList<KluisItem> Nagelaten = Array.Empty<KluisItem>();
|
||||||
private KluisInhoud? Inhoud;
|
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 string? GeopendTitel;
|
||||||
private IReadOnlyList<Bijlage> GeopendBijlagen = Array.Empty<Bijlage>();
|
private IReadOnlyList<Bijlage> GeopendBijlagen = Array.Empty<Bijlage>();
|
||||||
|
|
||||||
@@ -604,6 +607,16 @@ else if (Stand is { IsBevestigd: true })
|
|||||||
Inhoud = r.Waarde;
|
Inhoud = r.Waarde;
|
||||||
GeopendTitel = item.Title;
|
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
|
// Bijlagen zijn hier optioneel: als ophalen mislukt tonen we het item gewoon
|
||||||
// zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
|
// zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
|
||||||
var bijlagen = await Toestand.Client.BijlagenAsync(item.Id);
|
var bijlagen = await Toestand.Client.BijlagenAsync(item.Id);
|
||||||
@@ -616,6 +629,7 @@ else if (Stand is { IsBevestigd: true })
|
|||||||
{
|
{
|
||||||
GeopendItem = null;
|
GeopendItem = null;
|
||||||
Inhoud = null;
|
Inhoud = null;
|
||||||
|
GeopendSoort = null;
|
||||||
GeopendTitel = null;
|
GeopendTitel = null;
|
||||||
GeopendBijlagen = Array.Empty<Bijlage>();
|
GeopendBijlagen = Array.Empty<Bijlage>();
|
||||||
BijlageDichtdoen();
|
BijlageDichtdoen();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MijnBesluit.Domain.Entities;
|
using MijnBesluit.Application.Models;
|
||||||
|
using MijnBesluit.Domain.Entities;
|
||||||
using MijnBesluit.Domain.Enums;
|
using MijnBesluit.Domain.Enums;
|
||||||
|
|
||||||
namespace MijnBesluit.Application.Abstractions;
|
namespace MijnBesluit.Application.Abstractions;
|
||||||
@@ -261,6 +262,31 @@ public interface IAuditLog
|
|||||||
string? details = null, CancellationToken ct = default);
|
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>Versturen van uitnodigingen en meldingen.</summary>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adverteerders, hun campagnes en de tellers per uur. Zie docs/Adverteren.md.
|
/// Adverteerders, hun campagnes en de tellers per uur. Zie docs/Adverteren.md.
|
||||||
|
|||||||
@@ -0,0 +1,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? Music { get; set; }
|
||||||
public string? Speakers { get; set; }
|
public string? Speakers { get; set; }
|
||||||
public string? Wishes { 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 IUnitOfWork _uow;
|
||||||
private readonly IClock _clock;
|
private readonly IClock _clock;
|
||||||
private readonly IAfbeeldingVerkleiner _verkleiner;
|
private readonly IAfbeeldingVerkleiner _verkleiner;
|
||||||
|
private readonly VaultCategoryService? _soorten;
|
||||||
|
|
||||||
/// <summary>De langste zijde van de kleine versie die bij een foto wordt bewaard.</summary>
|
/// <summary>De langste zijde van de kleine versie die bij een foto wordt bewaard.</summary>
|
||||||
public const int ThumbnailZijde = 160;
|
public const int ThumbnailZijde = 160;
|
||||||
@@ -46,12 +47,14 @@ public class VaultService
|
|||||||
public VaultService(IVaultRepository vault, IPersonRepository people, IMessageRepository messages,
|
public VaultService(IVaultRepository vault, IPersonRepository people, IMessageRepository messages,
|
||||||
IUserRepository users, IMasterKeyProtector keys, IFileStore files, IAuditLog audit,
|
IUserRepository users, IMasterKeyProtector keys, IFileStore files, IAuditLog audit,
|
||||||
IUnitOfWork uow, IClock clock, IAfbeeldingVerkleiner? verkleiner = null,
|
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;
|
_vault = vault; _people = people; _messages = messages; _users = users; _keys = keys;
|
||||||
_files = files; _audit = audit; _uow = uow; _clock = clock; _families = families;
|
_files = files; _audit = audit; _uow = uow; _clock = clock; _families = families;
|
||||||
_verkleiner = verkleiner ?? new GeenAfbeeldingVerkleiner();
|
_verkleiner = verkleiner ?? new GeenAfbeeldingVerkleiner();
|
||||||
_legacy = legacy;
|
_legacy = legacy;
|
||||||
|
_soorten = soorten;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Titels en categorieën van de kluis. De inhoud blijft versleuteld tot hij echt geopend wordt.</summary>
|
/// <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)
|
if (release == ReleasePolicy.OpDatum && releaseAt is null)
|
||||||
return Result<VaultItem>.Fail("Kies een datum waarop dit item vrijkomt.");
|
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 now = _clock.UtcNow;
|
||||||
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
||||||
var contentKey = EnvelopeCrypto.NewKey();
|
var contentKey = EnvelopeCrypto.NewKey();
|
||||||
@@ -89,7 +96,7 @@ public class VaultService
|
|||||||
WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey),
|
WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey),
|
||||||
ReleasePolicy = release,
|
ReleasePolicy = release,
|
||||||
ReleaseAt = releaseAt,
|
ReleaseAt = releaseAt,
|
||||||
SharedManagerPersonId = KanBesprekenMet(category) ? sharedManagerPersonId : null,
|
SharedManagerPersonId = kanBespreken ? sharedManagerPersonId : null,
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
UpdatedAt = now
|
UpdatedAt = now
|
||||||
};
|
};
|
||||||
@@ -132,6 +139,12 @@ public class VaultService
|
|||||||
if (release == ReleasePolicy.OpDatum && releaseAt is null)
|
if (release == ReleasePolicy.OpDatum && releaseAt is null)
|
||||||
return Result.Fail("Kies een datum waarop dit item vrijkomt.");
|
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 now = _clock.UtcNow;
|
||||||
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
var dataKey = _keys.Unprotect(user.WrappedDataKey);
|
||||||
var contentKey = EnvelopeCrypto.NewKey();
|
var contentKey = EnvelopeCrypto.NewKey();
|
||||||
@@ -147,7 +160,7 @@ public class VaultService
|
|||||||
item.WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey);
|
item.WrappedContentKey = EnvelopeCrypto.WrapKey(contentKey, dataKey);
|
||||||
item.ReleasePolicy = release;
|
item.ReleasePolicy = release;
|
||||||
item.ReleaseAt = releaseAt;
|
item.ReleaseAt = releaseAt;
|
||||||
item.SharedManagerPersonId = KanBesprekenMet(category) ? sharedManagerPersonId : null;
|
item.SharedManagerPersonId = kanBespreken ? sharedManagerPersonId : null;
|
||||||
item.UpdatedAt = now;
|
item.UpdatedAt = now;
|
||||||
|
|
||||||
// Eerder verleende toegang was op de oude sleutel; die is nu waardeloos. Wie het item
|
// 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>
|
/// <summary>
|
||||||
/// Geeft de gekozen nalatenschapsbeheerder van een Uitvaartwens meteen leestoegang tot dit
|
/// 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
|
/// 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
|
/// 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.
|
/// beide, dan wordt de keuze stil overgeslagen; het item zelf blijft wel bewaard staan.
|
||||||
/// </summary>
|
/// </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)
|
private async Task DeelMetBeheerderAsync(VaultItem item, User owner, long beheerderPersonId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (_legacy is not null)
|
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) =>
|
public Task<Uitkomst<IReadOnlyList<KluisItem>>> KluisAsync(int? categorie = null, CancellationToken ct = default) =>
|
||||||
StuurAsync<IReadOnlyList<KluisItem>>(SocketActions.VaultList, new { category = categorie }, ct);
|
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) =>
|
public Task<Uitkomst<KluisInhoud>> KluisOpenenAsync(long itemId, CancellationToken ct = default) =>
|
||||||
StuurAsync<KluisInhoud>(SocketActions.VaultOpen, new { id = itemId }, ct);
|
StuurAsync<KluisInhoud>(SocketActions.VaultOpen, new { id = itemId }, ct);
|
||||||
|
|
||||||
|
|||||||
@@ -246,7 +246,58 @@ public record KluisItem(
|
|||||||
int ReleasePolicy,
|
int ReleasePolicy,
|
||||||
DateTime? ReleaseAt,
|
DateTime? ReleaseAt,
|
||||||
DateTime UpdatedAt,
|
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>
|
/// <summary>De ontsleutelde inhoud van een kluisitem. Per soort is maar een deel gevuld.</summary>
|
||||||
public class KluisInhoud
|
public class KluisInhoud
|
||||||
@@ -273,6 +324,34 @@ public class KluisInhoud
|
|||||||
public string? Music { get; set; }
|
public string? Music { get; set; }
|
||||||
public string? Speakers { get; set; }
|
public string? Speakers { get; set; }
|
||||||
public string? Wishes { 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>
|
/// <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,
|
OverlijdenGemeld = 50, OverlijdenBetwist = 51, OverlijdenBevestigd = 52,
|
||||||
NalatenschapToegangVerleend = 60,
|
NalatenschapToegangVerleend = 60,
|
||||||
AdvertentieIngediend = 70, AdvertentieBetaald = 71, AdvertentieGoedgekeurd = 72,
|
AdvertentieIngediend = 70, AdvertentieBetaald = 71, AdvertentieGoedgekeurd = 72,
|
||||||
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74
|
AdvertentieAfgewezen = 73, AdvertentieGestopt = 74,
|
||||||
|
KluisSoortBeheerd = 80
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IFamilyRepository, FamilyRepository>();
|
services.AddScoped<IFamilyRepository, FamilyRepository>();
|
||||||
services.AddScoped<IInvitationRepository, InvitationRepository>();
|
services.AddScoped<IInvitationRepository, InvitationRepository>();
|
||||||
services.AddScoped<IVaultRepository, VaultRepository>();
|
services.AddScoped<IVaultRepository, VaultRepository>();
|
||||||
|
services.AddScoped<IVaultCategoryRepository, VaultCategoryRepository>();
|
||||||
services.AddScoped<IMessageRepository, MessageRepository>();
|
services.AddScoped<IMessageRepository, MessageRepository>();
|
||||||
services.AddScoped<ILegacyRepository, LegacyRepository>();
|
services.AddScoped<ILegacyRepository, LegacyRepository>();
|
||||||
services.AddScoped<IToestelRepository, ToestelRepository>();
|
services.AddScoped<IToestelRepository, ToestelRepository>();
|
||||||
@@ -103,6 +104,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IAuditLog, EfAuditLog>();
|
services.AddScoped<IAuditLog, EfAuditLog>();
|
||||||
|
|
||||||
services.AddScoped<AccountService>();
|
services.AddScoped<AccountService>();
|
||||||
|
services.AddScoped<VaultCategoryService>();
|
||||||
services.AddScoped<VaultService>();
|
services.AddScoped<VaultService>();
|
||||||
// De link naar de landingspagina voor iemand zonder account moet naar de echte site wijzen.
|
// De link naar de landingspagina voor iemand zonder account moet naar de echte site wijzen.
|
||||||
services.AddScoped(sp =>
|
services.AddScoped(sp =>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ public class MijnBesluitDbContext : DbContext
|
|||||||
public DbSet<Invitation> Invitations => Set<Invitation>();
|
public DbSet<Invitation> Invitations => Set<Invitation>();
|
||||||
public DbSet<VaultItem> VaultItems => Set<VaultItem>();
|
public DbSet<VaultItem> VaultItems => Set<VaultItem>();
|
||||||
public DbSet<VaultFile> VaultFiles => Set<VaultFile>();
|
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<Toestel> Toestellen => Set<Toestel>();
|
||||||
public DbSet<Sessie> Sessies => Set<Sessie>();
|
public DbSet<Sessie> Sessies => Set<Sessie>();
|
||||||
public DbSet<ContentGrant> ContentGrants => Set<ContentGrant>();
|
public DbSet<ContentGrant> ContentGrants => Set<ContentGrant>();
|
||||||
@@ -218,6 +220,37 @@ public class MijnBesluitDbContext : DbContext
|
|||||||
e.HasOne(x => x.VaultFile).WithMany().HasForeignKey(x => x.VaultFileId);
|
e.HasOne(x => x.VaultFile).WithMany().HasForeignKey(x => x.VaultFileId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
b.Entity<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 =>
|
b.Entity<ContentGrant>(e =>
|
||||||
{
|
{
|
||||||
e.ToTable("content_grants");
|
e.ToTable("content_grants");
|
||||||
|
|||||||
@@ -229,6 +229,44 @@ public class InvitationRepository : IInvitationRepository
|
|||||||
public void Remove(Invitation invitation) => _db.Invitations.Remove(invitation);
|
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
|
public class VaultRepository : IVaultRepository
|
||||||
{
|
{
|
||||||
private readonly MijnBesluitDbContext _db;
|
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>
|
/// <summary>Kluisitems die iemand rechtstreeks met jou heeft gedeeld, vooraf te bespreken.</summary>
|
||||||
public const string VaultSharedWithMe = "vault.shared-with-me";
|
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>
|
/// <summary>Nalatenschappen die aan jou zijn toevertrouwd, en wat daarvan is vrijgegeven.</summary>
|
||||||
public const string LegacyEntrusted = "legacy.entrusted";
|
public const string LegacyEntrusted = "legacy.entrusted";
|
||||||
public const string LegacyEntrustedItems = "legacy.entrusted-items";
|
public const string LegacyEntrustedItems = "legacy.entrusted-items";
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ public class RequestScope
|
|||||||
public required LegacyService Legacy { get; init; }
|
public required LegacyService Legacy { get; init; }
|
||||||
public required OverzichtService Overzicht { 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>
|
/// <summary>
|
||||||
/// Voor het samenvoegen van dubbele profielen. Optioneel: zonder deze dienst werkt alles,
|
/// Voor het samenvoegen van dubbele profielen. Optioneel: zonder deze dienst werkt alles,
|
||||||
/// alleen wordt een tweede uitnodiging dan geweigerd in plaats van samengevoegd.
|
/// alleen wordt een tweede uitnodiging dan geweigerd in plaats van samengevoegd.
|
||||||
@@ -76,6 +82,17 @@ public class SocketDispatcher
|
|||||||
f.Id, f.FileName, f.ContentType, f.SizeBytes, f.CreatedAt, f.IsProfilePhoto
|
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>
|
/// <summary>Voor als er geen klok wordt meegegeven; de overdracht heeft er wel een nodig.</summary>
|
||||||
private sealed class SysteemKlok : MijnBesluit.Application.Abstractions.IClock
|
private sealed class SysteemKlok : MijnBesluit.Application.Abstractions.IClock
|
||||||
{
|
{
|
||||||
@@ -451,12 +468,31 @@ public class SocketDispatcher
|
|||||||
{
|
{
|
||||||
var p = request.PayloadAs<VaultListPayload>();
|
var p = request.PayloadAs<VaultListPayload>();
|
||||||
var items = await scope.Vault.ListAsync(session!.PersonId, p?.Category, ct);
|
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
|
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:
|
case SocketActions.VaultOpen:
|
||||||
{
|
{
|
||||||
var p = request.PayloadAs<IdPayload>();
|
var p = request.PayloadAs<IdPayload>();
|
||||||
@@ -807,10 +843,12 @@ public class SocketDispatcher
|
|||||||
var r = await scope.Legacy.NalatenschapItemsAsync(p.PersonId, session!.UserId, ct);
|
var r = await scope.Legacy.NalatenschapItemsAsync(p.PersonId, session!.UserId, ct);
|
||||||
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
||||||
|
|
||||||
|
var nalatenschapNamen = await SoortNamenAsync(scope, ct);
|
||||||
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
||||||
{
|
{
|
||||||
i.Id, i.Title, Category = (int)i.Category, ReleasePolicy = (int)i.ReleasePolicy,
|
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);
|
var r = await scope.Vault.ListSharedWithMeAsync(session!.UserId, ct);
|
||||||
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
if (!r.Succeeded) return SocketResponse.Error(id, r.Error!);
|
||||||
|
|
||||||
|
var gedeeldNamen = await SoortNamenAsync(scope, ct);
|
||||||
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
return SocketResponse.Ok(id, r.Value!.Select(i => new
|
||||||
{
|
{
|
||||||
i.Id, i.Title, Category = (int)i.Category, ReleasePolicy = (int)i.ReleasePolicy,
|
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>(),
|
Accounts = scope.ServiceProvider.GetRequiredService<AccountService>(),
|
||||||
Families = scope.ServiceProvider.GetRequiredService<FamilyService>(),
|
Families = scope.ServiceProvider.GetRequiredService<FamilyService>(),
|
||||||
Vault = scope.ServiceProvider.GetRequiredService<VaultService>(),
|
Vault = scope.ServiceProvider.GetRequiredService<VaultService>(),
|
||||||
|
Soorten = scope.ServiceProvider.GetRequiredService<VaultCategoryService>(),
|
||||||
Messages = scope.ServiceProvider.GetRequiredService<MessageService>(),
|
Messages = scope.ServiceProvider.GetRequiredService<MessageService>(),
|
||||||
Legacy = scope.ServiceProvider.GetRequiredService<LegacyService>(),
|
Legacy = scope.ServiceProvider.GetRequiredService<LegacyService>(),
|
||||||
Meldingen = scope.ServiceProvider.GetRequiredService<MeldingService>(),
|
Meldingen = scope.ServiceProvider.GetRequiredService<MeldingService>(),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using MijnBesluit.Application.Models;
|
using MijnBesluit.Application.Models;
|
||||||
|
using MijnBesluit.Domain.Entities;
|
||||||
using MijnBesluit.Domain.Enums;
|
using MijnBesluit.Domain.Enums;
|
||||||
|
using MijnBesluit.Domain.Rules;
|
||||||
|
|
||||||
namespace MijnBesluit.Web.Components.Onderdelen;
|
namespace MijnBesluit.Web.Components.Onderdelen;
|
||||||
|
|
||||||
@@ -14,8 +16,11 @@ namespace MijnBesluit.Web.Components.Onderdelen;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Kluisweergave
|
public static class Kluisweergave
|
||||||
{
|
{
|
||||||
/// <summary>De ingevulde velden van een item, in de volgorde waarin je ze wilt lezen.</summary>
|
/// <summary>
|
||||||
public static IEnumerable<(string Label, string Waarde)> Velden(VaultPayload p)
|
/// 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.Website is not null) yield return ("Website", p.Website);
|
||||||
if (p.Username is not null) yield return ("Gebruikersnaam", p.Username);
|
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.Music is not null) yield return ("Muziek", p.Music);
|
||||||
if (p.Speakers is not null) yield return ("Sprekers", p.Speakers);
|
if (p.Speakers is not null) yield return ("Sprekers", p.Speakers);
|
||||||
if (p.Wishes is not null) yield return ("Wensen", p.Wishes);
|
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);
|
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
|
public static string Naam(VaultCategory c) => c switch
|
||||||
{
|
{
|
||||||
VaultCategory.Uitvaartwens => "Uitvaartwens",
|
VaultCategory.Uitvaartwens => "Uitvaartwens",
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ else
|
|||||||
hoge uitzondering. *@
|
hoge uitzondering. *@
|
||||||
<p><a href="/beheer/adverteren">Advertenties beheren ›</a></p>
|
<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> }
|
@if (Melding is not null) { <div class="melding @MeldingSoort">@Melding</div> }
|
||||||
|
|
||||||
@* ------------------------------------------------------------ statistieken *@
|
@* ------------------------------------------------------------ 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]
|
@attribute [Authorize]
|
||||||
@inject VaultService KluisService
|
@inject VaultService KluisService
|
||||||
@inject LegacyService BeheerService
|
@inject LegacyService BeheerService
|
||||||
|
@inject VaultCategoryService SoortService
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
@@ -21,9 +22,9 @@
|
|||||||
<label for="cat">Soort</label>
|
<label for="cat">Soort</label>
|
||||||
<select id="cat" @bind="Filter" @bind:after="Verversen">
|
<select id="cat" @bind="Filter" @bind:after="Verversen">
|
||||||
<option value="">Alles</option>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,9 +44,13 @@
|
|||||||
OpBevestigen="Opslaan" OpSluiten="Annuleren">
|
OpBevestigen="Opslaan" OpSluiten="Annuleren">
|
||||||
<div class="veld">
|
<div class="veld">
|
||||||
<label for="soort">Wat wil je vastleggen?</label>
|
<label for="soort">Wat wil je vastleggen?</label>
|
||||||
<InputSelect id="soort" @bind-Value="Nieuw.Categorie">
|
<InputSelect id="soort" @bind-Value="Nieuw.CategorieNummer">
|
||||||
@foreach (var c in Categorieen) { <option value="@c">@Naam(c)</option> }
|
@foreach (var s in Soorten) { <option value="@s.Id">@s.Naam</option> }
|
||||||
</InputSelect>
|
</InputSelect>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(HuidigeSoort?.Uitleg))
|
||||||
|
{
|
||||||
|
<div class="uitleg">@HuidigeSoort!.Uitleg</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="veld">
|
<div class="veld">
|
||||||
<label for="titel">Titel</label>
|
<label for="titel">Titel</label>
|
||||||
@@ -129,9 +134,20 @@
|
|||||||
|
|
||||||
case VaultCategory.Donorcodicil:
|
case VaultCategory.Donorcodicil:
|
||||||
break;
|
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">
|
<div class="veld">
|
||||||
<label>Bespreken met</label>
|
<label>Bespreken met</label>
|
||||||
@@ -256,7 +272,7 @@ else
|
|||||||
{
|
{
|
||||||
<hr />
|
<hr />
|
||||||
<dl style="margin:0">
|
<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">
|
<div class="rij" style="padding:.25rem 0">
|
||||||
<span class="zacht" style="min-width:150px">@label</span>
|
<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>
|
/// <summary>Zoveel bestanden tegelijk kiezen mag. Blazor wil hier een bovengrens.</summary>
|
||||||
private const int MaxTegelijk = 10;
|
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,
|
Soorten = await SoortService.ZichtbaarVoorAsync(_persoonId);
|
||||||
VaultCategory.SocialMedia, VaultCategory.Apparaat, VaultCategory.Pincode,
|
Namen = await SoortService.NamenAsync();
|
||||||
VaultCategory.Rijbewijs, VaultCategory.Paspoort,
|
}
|
||||||
VaultCategory.Document, VaultCategory.Notitie, VaultCategory.Overig
|
|
||||||
};
|
/// <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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -382,6 +470,8 @@ else
|
|||||||
_gebruikerId = user.UserId() ?? 0;
|
_gebruikerId = user.UserId() ?? 0;
|
||||||
_persoonId = user.PersonId() ?? 0;
|
_persoonId = user.PersonId() ?? 0;
|
||||||
Beheerders = await BeheerService.ListManagersAsync(_persoonId);
|
Beheerders = await BeheerService.ListManagersAsync(_persoonId);
|
||||||
|
await SoortenLaden();
|
||||||
|
Nieuw = LeegItem();
|
||||||
await Verversen();
|
await Verversen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +492,8 @@ else
|
|||||||
{
|
{
|
||||||
Geopend = id;
|
Geopend = id;
|
||||||
Inhoud = resultaat.Value;
|
Inhoud = resultaat.Value;
|
||||||
|
var gekozen = Items?.FirstOrDefault(i => i.Id == id);
|
||||||
|
GeopendSoort = gekozen is null ? null : await SoortService.SoortAsync((int)gekozen.Category);
|
||||||
Melding = null;
|
Melding = null;
|
||||||
await BijlagenLaden();
|
await BijlagenLaden();
|
||||||
}
|
}
|
||||||
@@ -514,6 +606,7 @@ else
|
|||||||
Muziek = p.Music ?? "",
|
Muziek = p.Music ?? "",
|
||||||
Sprekers = p.Speakers ?? "",
|
Sprekers = p.Speakers ?? "",
|
||||||
Wensen = p.Wishes ?? "",
|
Wensen = p.Wishes ?? "",
|
||||||
|
Velden = p.Velden is null ? new Dictionary<string, string>() : new Dictionary<string, string>(p.Velden),
|
||||||
BesprekenMetPersonId = item.SharedManagerPersonId,
|
BesprekenMetPersonId = item.SharedManagerPersonId,
|
||||||
Vrijgave = item.ReleasePolicy,
|
Vrijgave = item.ReleasePolicy,
|
||||||
VrijgaveDatum = item.ReleaseAt is { } r ? DateOnly.FromDateTime(r) : null
|
VrijgaveDatum = item.ReleaseAt is { } r ? DateOnly.FromDateTime(r) : null
|
||||||
@@ -533,7 +626,7 @@ else
|
|||||||
private void Beginnen()
|
private void Beginnen()
|
||||||
{
|
{
|
||||||
BewerkId = 0;
|
BewerkId = 0;
|
||||||
Nieuw = new NieuwItem();
|
Nieuw = LeegItem();
|
||||||
Bijlagen = null;
|
Bijlagen = null;
|
||||||
ToonFormulier = true;
|
ToonFormulier = true;
|
||||||
}
|
}
|
||||||
@@ -541,7 +634,7 @@ else
|
|||||||
private void Annuleren()
|
private void Annuleren()
|
||||||
{
|
{
|
||||||
BewerkId = 0;
|
BewerkId = 0;
|
||||||
Nieuw = new NieuwItem();
|
Nieuw = LeegItem();
|
||||||
Bijlagen = null;
|
Bijlagen = null;
|
||||||
ToonFormulier = false;
|
ToonFormulier = false;
|
||||||
}
|
}
|
||||||
@@ -575,7 +668,13 @@ else
|
|||||||
Location = Leeg(Nieuw.Locatie),
|
Location = Leeg(Nieuw.Locatie),
|
||||||
Music = Leeg(Nieuw.Muziek),
|
Music = Leeg(Nieuw.Muziek),
|
||||||
Speakers = Leeg(Nieuw.Sprekers),
|
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
|
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.";
|
Melding = BewerkId == 0 ? "Opgeslagen in je kluis." : "De wijziging is opgeslagen.";
|
||||||
MeldingSoort = "goed";
|
MeldingSoort = "goed";
|
||||||
BewerkId = 0;
|
BewerkId = 0;
|
||||||
Nieuw = new NieuwItem();
|
Nieuw = LeegItem();
|
||||||
Bijlagen = null;
|
Bijlagen = null;
|
||||||
ToonFormulier = false;
|
ToonFormulier = false;
|
||||||
|
await SoortenLaden();
|
||||||
await Verversen();
|
await Verversen();
|
||||||
}
|
}
|
||||||
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
else { Melding = resultaat.Error; MeldingSoort = "fout"; }
|
||||||
@@ -615,8 +715,7 @@ else
|
|||||||
|
|
||||||
// Velden() en Naam() staan in Onderdelen/Kluisweergave.cs: de nalatenschapspagina toont
|
// 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.
|
// 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 string Naam(VaultCategory c) => Kluisweergave.Naam(c, Namen);
|
||||||
private static string Naam(VaultCategory c) => Kluisweergave.Naam(c);
|
|
||||||
|
|
||||||
private static string Voorbeeld(VaultCategory c) => c switch
|
private static string Voorbeeld(VaultCategory c) => c switch
|
||||||
{
|
{
|
||||||
@@ -640,6 +739,19 @@ else
|
|||||||
public class NieuwItem
|
public class NieuwItem
|
||||||
{
|
{
|
||||||
public VaultCategory Categorie { get; set; } = VaultCategory.Uitvaartwens;
|
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 Titel { get; set; } = "";
|
||||||
public string? Notitie { get; set; }
|
public string? Notitie { get; set; }
|
||||||
public string? Website { get; set; }
|
public string? Website { get; set; }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@inject ILegacyRepository LegacyOpslag
|
@inject ILegacyRepository LegacyOpslag
|
||||||
@inject IPersonRepository Personen
|
@inject IPersonRepository Personen
|
||||||
@inject VaultService Kluis
|
@inject VaultService Kluis
|
||||||
|
@inject VaultCategoryService SoortService
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
<PageTitle>Nalatenschap - MijnBesluit</PageTitle>
|
<PageTitle>Nalatenschap - MijnBesluit</PageTitle>
|
||||||
@@ -126,7 +127,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
|||||||
<div class="rij">
|
<div class="rij">
|
||||||
<div class="groei">
|
<div class="groei">
|
||||||
<span>@item.Title</span>
|
<span>@item.Title</span>
|
||||||
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
|
<div class="zacht">@Kluisweergave.Naam(item.Category, Namen)</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +156,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
|||||||
<div class="rij">
|
<div class="rij">
|
||||||
<div class="groei">
|
<div class="groei">
|
||||||
<span>@item.Title</span>
|
<span>@item.Title</span>
|
||||||
<div class="zacht">@Kluisweergave.Naam(item.Category)</div>
|
<div class="zacht">@Kluisweergave.Naam(item.Category, Namen)</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
<button class="knop rustig" @onclick="() => ItemOpenen(item)">Openen</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +174,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
|||||||
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
<Popup Titel="@GeopendTitel" Bevestigtekst="Sluiten" AlleenSluiten="true"
|
||||||
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
OpBevestigen="ItemDichtdoen" OpSluiten="ItemDichtdoen">
|
||||||
<div class="stapel">
|
<div class="stapel">
|
||||||
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud))
|
@foreach (var (label, waarde) in Kluisweergave.Velden(Inhoud, GeopendSoort))
|
||||||
{
|
{
|
||||||
<div class="veld">
|
<div class="veld">
|
||||||
<label>@label</label>
|
<label>@label</label>
|
||||||
@@ -388,6 +389,12 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
|||||||
private IReadOnlyList<VaultItem>? Nagelaten;
|
private IReadOnlyList<VaultItem>? Nagelaten;
|
||||||
private VaultPayload? Inhoud;
|
private VaultPayload? Inhoud;
|
||||||
private string? GeopendTitel;
|
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;
|
private IReadOnlyList<VaultFile>? GeopendBijlagen;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -397,6 +404,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
|||||||
_gebruikerId = user.UserId() ?? 0;
|
_gebruikerId = user.UserId() ?? 0;
|
||||||
_persoonId = user.PersonId() ?? 0;
|
_persoonId = user.PersonId() ?? 0;
|
||||||
|
|
||||||
|
Namen = await SoortService.NamenAsync();
|
||||||
_familieId = (await FamilieOpslag.ListOwnedByPersonAsync(_persoonId)).FirstOrDefault()?.Id ?? 0;
|
_familieId = (await FamilieOpslag.ListOwnedByPersonAsync(_persoonId)).FirstOrDefault()?.Id ?? 0;
|
||||||
if (_familieId != 0) Leden = await Families.ListMembersAsync(_familieId, _gebruikerId);
|
if (_familieId != 0) Leden = await Families.ListMembersAsync(_familieId, _gebruikerId);
|
||||||
|
|
||||||
@@ -465,6 +473,9 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
|||||||
Inhoud = resultaat.Value;
|
Inhoud = resultaat.Value;
|
||||||
GeopendTitel = item.Title;
|
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
|
// Bijlagen zijn optioneel: als ophalen mislukt (bv. geen toegang) tonen we het
|
||||||
// item gewoon zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
|
// item gewoon zonder bijlagenlijst, in plaats van de hele popup te blokkeren.
|
||||||
var bijlagen = await Kluis.ListFilesAsync(item.Id, _gebruikerId);
|
var bijlagen = await Kluis.ListFilesAsync(item.Id, _gebruikerId);
|
||||||
@@ -476,6 +487,7 @@ else if (Stand.State == DeathProcedureState.Lopend)
|
|||||||
GeopendItem = null;
|
GeopendItem = null;
|
||||||
Inhoud = null;
|
Inhoud = null;
|
||||||
GeopendTitel = null;
|
GeopendTitel = null;
|
||||||
|
GeopendSoort = null;
|
||||||
GeopendBijlagen = null;
|
GeopendBijlagen = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public static class ClientChecks
|
|||||||
Accounts = Wereld.Accounts,
|
Accounts = Wereld.Accounts,
|
||||||
Families = Wereld.Families,
|
Families = Wereld.Families,
|
||||||
Vault = Wereld.Vault,
|
Vault = Wereld.Vault,
|
||||||
|
Soorten = Wereld.Soorten,
|
||||||
Messages = Wereld.Messages,
|
Messages = Wereld.Messages,
|
||||||
Legacy = Wereld.Legacy,
|
Legacy = Wereld.Legacy,
|
||||||
Overzicht = Wereld.Overzicht,
|
Overzicht = Wereld.Overzicht,
|
||||||
@@ -64,6 +65,67 @@ public static class ClientChecks
|
|||||||
|
|
||||||
public static void Register(Runner runner)
|
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 () =>
|
runner.TestAsync("App: registreren met een code en de sessie bewaren", async () =>
|
||||||
{
|
{
|
||||||
await using var proef = new Proef();
|
await using var proef = new Proef();
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public class Store
|
|||||||
public List<Adverteerder> Adverteerders = new();
|
public List<Adverteerder> Adverteerders = new();
|
||||||
public List<Campagne> Campagnes = new();
|
public List<Campagne> Campagnes = new();
|
||||||
public List<CampagneUur> CampagneUren = new();
|
public List<CampagneUur> CampagneUren = new();
|
||||||
|
public List<VaultCategoryDefinition> Soorten = VaultCategorySaatgoed.Maak();
|
||||||
|
public List<VaultCategoryField> SoortVelden = new();
|
||||||
|
|
||||||
private long _seq;
|
private long _seq;
|
||||||
public long NextId() => ++_seq;
|
public long NextId() => ++_seq;
|
||||||
@@ -528,3 +530,66 @@ public class TestPush : IPushSender
|
|||||||
return Task.FromResult(true);
|
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);
|
ServiceChecks.Register(runner);
|
||||||
AdvertentieChecks.Register(runner);
|
AdvertentieChecks.Register(runner);
|
||||||
UitrolChecks.Register(runner);
|
UitrolChecks.Register(runner);
|
||||||
|
SoortenChecks.Register(runner);
|
||||||
|
|
||||||
return runner.Summary();
|
return runner.Summary();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class World
|
|||||||
public BeheerService Beheer { get; }
|
public BeheerService Beheer { get; }
|
||||||
public AdvertentieService Adverteren { get; }
|
public AdvertentieService Adverteren { get; }
|
||||||
public AccountOpheffenService Opheffen { 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>
|
/// <summary>Maakt in één stap een account aan; scheelt in de scenario's het hele codegedoe.</summary>
|
||||||
public async Task<MijnBesluit.Domain.Entities.User> MaakGebruikerAsync(string email, string voornaam, string achternaam = "")
|
public async Task<MijnBesluit.Domain.Entities.User> MaakGebruikerAsync(string email, string voornaam, string achternaam = "")
|
||||||
@@ -106,7 +107,8 @@ public class World
|
|||||||
Accounts = new AccountService(users, people, families, codes, keys, Email, audit, uow, Clock, vault);
|
Accounts = new AccountService(users, people, families, codes, keys, Email, audit, uow, Clock, vault);
|
||||||
Families = new FamilyService(families, people, users, invitations, messages, legacy,
|
Families = new FamilyService(families, people, users, invitations, messages, legacy,
|
||||||
Email, audit, uow, Clock);
|
Email, audit, uow, Clock);
|
||||||
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);
|
Meldingen = new MeldingService(new ToestelRepo(Store), users, Push, uow, Clock);
|
||||||
Messages = new MessageService(messages, people, users, vault, keys, files, Email, audit, uow, Clock, Meldingen);
|
Messages = new MessageService(messages, people, users, vault, keys, files, Email, audit, uow, Clock, Meldingen);
|
||||||
Legacy = new LegacyService(legacy, people, users, vault, messages, Email, audit, uow, Clock, Vault, families);
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user