- 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
439 lines
22 KiB
C#
439 lines
22 KiB
C#
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());
|
|
}
|