using System.Text.Json.Serialization;
namespace MijnBesluit.Client;
// De vorm van wat de SocketServer terugstuurt. De namen komen overeen met wat de dispatcher
// verstuurt; het protocol gebruikt kleine beginletters, dat regelt de JSON-instelling.
public record SessieGegevens(
[property: JsonPropertyName("token")] string Token,
[property: JsonPropertyName("persoonId")] long PersoonId,
[property: JsonPropertyName("verlooptOp")] DateTime? VerlooptOp);
public record CodeGestuurd(
[property: JsonPropertyName("verstuurdNaar")] string VerstuurdNaar,
[property: JsonPropertyName("geldigTot")] DateTime GeldigTot);
public record FamilieOverzicht(long Id, string Name, bool Eigenaar);
/// Eén regel op het beginscherm: iets wat speelt of eraan komt.
public record Overzichtsregel(string Soort, string Tekst, DateTime? Datum, long? Id)
{
public bool IsWaarschuwing => Soort == "overlijden";
public bool IsMoment => Soort == "moment";
}
/// Alles wat het beginscherm laat zien.
public record Overzicht(
int AantalKluis,
int AantalFamilie,
int AantalVoorMij,
int AantalVanMij,
int AantalNieuweBerichten,
int AantalBeheerders,
int VooruitblikDagen,
IReadOnlyList Regels);
///
/// De sociale media die je bij een kluisitem kunt kiezen.
///
/// Dit is een kopie van SocialeMedia in MijnBesluit.Domain: de app ziet het domein niet, want
/// die praat alleen over de socket. Een controle in de zelftest bewaakt dat de twee lijsten
/// gelijk blijven, zodat ze niet stilletjes uit elkaar lopen.
///
public static class SocialeMedia
{
public const string Anders = "Anders";
public static readonly string[] Namen =
{
"Facebook", "Instagram", "WhatsApp", "LinkedIn", "X (voorheen Twitter)",
"TikTok", "YouTube", "Snapchat", "Pinterest", "Telegram", "Signal",
"Discord", "Reddit", "Strava", "Marktplaats", Anders
};
}
///
/// Datums zoals ze op het scherm horen te staan, in het Nederlands.
///
/// Dit is een kopie van Datums in MijnBesluit.Domain: de app ziet het domein niet. Een controle
/// in de zelftest bewaakt dat de twee gelijk blijven.
///
/// Waarom dit met de hand gaat en niet met ToString("d MMMM yyyy"): dat volgt de taalinstelling
/// van het toestel, en dan leest een telefoon die op Engels staat "9 June 2031" terwijl de rest
/// van het scherm Nederlands is. Een app die maar in één taal bestaat, hoort ook maar in één taal
/// te rekenen.
///
public static class Datums
{
public static readonly string[] Maanden =
{
"januari", "februari", "maart", "april", "mei", "juni",
"juli", "augustus", "september", "oktober", "november", "december"
};
/// Afkortingen voor waar het krap is. "Mei" heeft geen punt: dat is geen afkorting.
public static readonly string[] KorteMaanden =
{
"Jan.", "Feb.", "Mrt.", "Apr.", "Mei", "Jun.",
"Jul.", "Aug.", "Sep.", "Okt.", "Nov.", "Dec."
};
/// Zondag eerst, want zo telt DayOfWeek.
public static readonly string[] Dagen =
{
"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"
};
}
///
/// Wat iemand van je is: de lijst die je te kiezen krijgt, en hoe elke relatie heet.
///
/// Dit is een kopie van Relaties in MijnBesluit.Domain: de app ziet het domein niet, want die
/// praat alleen over de socket. Daarom staan hier nummers en geen enum - dat is wat er over de
/// lijn gaat. Een controle in de zelftest bewaakt dat de twee lijsten gelijk blijven.
///
/// is wat je kunt kiezen, vertaalt élke waarde die ooit
/// heeft bestaan. Kind, Bonuskind, Ouder, Bonusouder, Kleinkind en Grootouder kun je niet meer
/// kiezen, maar staan wel bij bestaande mensen in de database; zonder vertaling zou daar ineens
/// niets meer staan.
///
public static class Relaties
{
public static readonly (int Waarde, string Naam)[] Keuzes =
{
(1, "Partner"),
(11, "Zoon"),
(12, "Dochter"),
(28, "Bonuszoon"),
(29, "Bonusdochter"),
(16, "Schoonzoon"),
(15, "Schoondochter"),
(14, "Kleinzoon"),
(13, "Kleindochter"),
(33, "Achterkleinzoon"),
(32, "Achterkleindochter"),
(19, "Vader"),
(20, "Moeder"),
(22, "Bonusvader"),
(21, "Bonusmoeder"),
(24, "Schoonvader"),
(25, "Schoonmoeder"),
(17, "Opa"),
(18, "Oma"),
(6, "Broer"),
(7, "Zus"),
(30, "Bonusbroer"),
(31, "Bonuszus"),
(26, "Zwager"),
(27, "Schoonzus"),
(34, "Oom"),
(35, "Tante"),
(10, "Vriend"),
(23, "Vriendin"),
(99, "Anders")
};
/// Hoe een relatie op het scherm heet - ook de relaties die je niet meer kunt kiezen.
public static string Naam(int relatie) => relatie switch
{
2 => "Kind",
3 => "Bonuskind",
4 => "Ouder",
5 => "Bonusouder",
8 => "Kleinkind",
9 => "Grootouder",
0 => "Familie",
_ => Keuzes.FirstOrDefault(k => k.Waarde == relatie).Naam ?? "Familie"
};
}
///
/// Een profiel dat iemand anders voor je heeft aangemaakt op jouw e-mailadres, en waarvan alleen
/// jij kunt zeggen of het over jou gaat. Zie docs/Identiteit.md.
///
public record Aantekening(long PersoonId, string Van, string? Relatie, DateTime AangemaaktOp);
/// Eigendomsstatus van een profiel: 0 beheerd, 1 uitgenodigd, 2 geclaimd.
public enum ProfielStand { Beheerd = 0, Uitgenodigd = 1, Geclaimd = 2 }
public record Familielid(
long PersonId,
string DisplayName,
string FirstName,
string LastName,
string? Email,
int Relation,
string? RelationLabel,
ProfielStand State,
bool CanEdit,
DateOnly? BirthDate,
DateOnly? DateOfDeath,
// De geschiedenis van het profiel. Komt rechtstreeks van de server; alles in UTC.
DateTime CreatedAt,
DateTime? InvitedAt,
DateTime? InvitationExpiresAt,
int InvitationResends,
DateTime? ClaimedAt,
// Twee dingen die pas ter sprake komen als je iemand weghaalt, maar die meekomen met de
// lijst zodat het scherm ze meteen heeft. Zie FamilyService.VerwijderLidAsync.
bool IsBeheerder,
int MomentenKlaar,
// De profielfoto. Het nummer om hem groot op te halen, en de kleine versie als base64 om
// hem meteen te kunnen tonen zonder tweede ronde langs de server.
long? ProfielfotoId = null,
string? Profielfoto = null)
{
public bool IsGeclaimd => State == ProfielStand.Geclaimd;
/// De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.
public string? Profielfotobron =>
string.IsNullOrEmpty(Profielfoto) ? null : $"data:image/jpeg;base64,{Profielfoto}";
}
///
/// Wat er verdwijnt als je je account opheft. Staat in het scherm vóórdat iemand ja zegt:
/// "weet je het zeker?" is een lege vraag, "je kluis met 23 items" is er een waar iemand echt
/// over nadenkt.
///
public record Opheffing(int Kluisitems, int Bestanden, int BerichtenKlaar, int BerichtenBezorgd,
int Mensen, int Nalatenschappen);
/// Je eigen gegevens, zoals het profielscherm ze toont.
public record EigenProfiel(long PersoonId, string Voornaam, string Achternaam, string Email,
DateOnly? Geboortedatum, DateTime LidSinds,
// De kleine versie van je eigen profielfoto, als base64. Die gaat mee
// in dit antwoord omdat de app hem op meer schermen laat zien dan
// alleen je profiel - in de kluis staat hij bij "Alleen voor mijzelf".
// Een eigen ronde langs de server voor een plaatje van veertig pixels
// zou duurder zijn dan het plaatje zelf.
string? Profielfoto = null)
{
public string Naam => string.Join(" ",
new[] { Voornaam, Achternaam }.Where(d => !string.IsNullOrWhiteSpace(d)));
/// De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.
public string? Profielfotobron =>
string.IsNullOrEmpty(Profielfoto) ? null : $"data:image/jpeg;base64,{Profielfoto}";
}
///
/// Een nalatenschap die jij beheert. is false zolang de persoon er
/// gewoon is: dan weet je alleen dat je bent aangewezen en valt er niets in te zien.
///
public record Toevertrouwd(long PersonId, string DisplayName, DateOnly? DateOfDeath,
DateTime? AccessGrantedAt, int AantalItems,
bool Afgerond = false, int Volgorde = 1, string? Profielfoto = null,
int RequiredDeclarations = 3, int ObjectionDays = 14)
{
/// De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.
public string? Profielfotobron =>
string.IsNullOrEmpty(Profielfoto) ? null : $"data:image/jpeg;base64,{Profielfoto}";
}
public record KluisItem(
long Id,
string Title,
int Category,
int ReleasePolicy,
DateTime? ReleaseAt,
DateTime UpdatedAt,
long? SharedManagerPersonId = null,
string? CategoryName = null)
{
///
/// 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.
///
public string SoortNaam(Func ingebouwd) =>
string.IsNullOrWhiteSpace(CategoryName) ? ingebouwd(Category) : CategoryName;
}
///
/// 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 ; een soort die de
/// beheerder maakte heeft een nummer vanaf 100 en beschrijft zijn formulier hier.
///
public record KluisSoort(int Id, string Naam, string? Uitleg, bool BesprekenMet, bool Ingebouwd,
IReadOnlyList Velden);
/// Eén veld van het formulier van een soort die de beheerder maakte.
public record KluisVeld(string Sleutel, string Label, string Soort, IReadOnlyList 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";
}
///
/// 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.
///
public static class IngebouwdeSoorten
{
private static KluisSoort S(int id, string naam, bool bespreken = false) =>
new(id, naam, null, bespreken, true, Array.Empty());
public static readonly IReadOnlyList 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")
};
/// De naam bij een nummer; "Overig" voor wat we niet kennen.
public static string Naam(int id) => Lijst.FirstOrDefault(s => s.Id == id)?.Naam ?? "Overig";
}
/// De ontsleutelde inhoud van een kluisitem. Per soort is maar een deel gevuld.
public class KluisInhoud
{
public string? Notes { get; set; }
public string? Website { get; set; }
public string? Username { get; set; }
public string? Secret { get; set; }
public string? Pincode { get; set; }
public string? DeviceName { get; set; }
public string? RecoveryCodes { get; set; }
/// Welk sociaal medium; een van SocialeMedia.Namen, of "Anders".
public string? Platform { get; set; }
/// Alleen gevuld als Platform "Anders" is.
public string? PlatformAnders { get; set; }
public string? Insurer { get; set; }
public string? PolicyNumber { get; set; }
public string? ContactPhone { get; set; }
public decimal? Amount { get; set; }
public string? BurialOrCremation { get; set; }
public string? Location { get; set; }
public string? Music { get; set; }
public string? Speakers { get; set; }
public string? Wishes { get; set; }
///
/// De velden van een soort die de beheerder zelf maakte, op de vaste sleutel van het veld.
/// Bij de ingebouwde soorten blijft dit leeg.
///
public Dictionary Velden { get; set; } = new();
///
/// 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.
///
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);
}
}
}
/// Een bijlage bij een kluisitem, zonder de inhoud zelf.
public record Bijlage(long Id, string FileName, string ContentType, long SizeBytes, DateTime CreatedAt,
bool IsProfilePhoto = false)
{
/// De grootte zoals je hem aan iemand laat zien: "1,4 MB".
public string Grootte => SizeBytes >= 1024 * 1024
? $"{SizeBytes / 1024d / 1024d:0.#} MB"
: $"{Math.Max(1, SizeBytes / 1024)} kB";
public bool IsAfbeelding => ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
public bool IsVideo => ContentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase);
}
/// Een opgehaald bestand, met de inhoud erbij.
public record BestandInhoud(string Naam, string Soort, byte[] Inhoud);
public record Bericht(
long Id,
string Subject,
int Kind,
long FromPersonId,
long ToPersonId,
DateTime? DeliveredAt,
DateTime? ReadAt,
DateTime? DeliverAt,
bool DeliverAfterDeath,
int Status,
long? VaultFileId)
{
public bool IsNieuw => Status == 2; // 2 = bezorgd, 3 = gelezen
/// 0 tekst, 1 video, 2 audio, 3 document.
public bool IsVideo => Kind == 1;
/// Zit er een opname of bijlage bij dit bericht?
public bool HeeftBestand => VaultFileId is > 0;
}
/// De uitkomst van een proefmelding: is hij verstuurd, en zo niet waarom niet.
public record Proefmelding(bool Gelukt, string Uitleg);
/// Iemand die jouw nalatenschap mag beheren. Volgorde 1 is de eerste keus.
///
/// De advertentie die nu op het overzicht mag staan. De adressen zijn volledig: de app weet niet
/// waar de site staat en hoort dat ook niet zelf in elkaar te zetten.
///
///
/// Gaat naar de site en niet rechtstreeks naar de adverteerder. Daar wordt de klik geteld en
/// vandaar wordt doorgestuurd; zo is er één plek waar dat gebeurt.
///
public record Advertentie(long Id, string Tekst, string? Afbeelding, string Link);
public record Beheerder(long PersonId, string DisplayName, int Priority, DateTime? AccessGrantedAt,
string? Profielfoto = null)
{
public bool HeeftToegang => AccessGrantedAt is not null;
/// De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.
public string? Profielfotobron =>
string.IsNullOrEmpty(Profielfoto) ? null : $"data:image/jpeg;base64,{Profielfoto}";
}
public record NalatenschapStand(
int State,
int ActiveDeclarations,
int RequiredDeclarations,
DateTime? ObjectionDeadline,
int Missing,
int ObjectionDays = 14,
bool BeheerderHeeftGemeld = false,
bool WachtOpBeheerder = false)
{
public bool LooptEenProcedure => State == 1;
public bool IsBevestigd => State == 3;
}