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; }