Eerste commit

This commit is contained in:
jf
2026-09-27 14:21:31 +00:00
commit de4ff128cb
295 changed files with 47374 additions and 0 deletions
+352
View File
@@ -0,0 +1,352 @@
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);
/// <summary>Eén regel op het beginscherm: iets wat speelt of eraan komt.</summary>
public record Overzichtsregel(string Soort, string Tekst, DateTime? Datum, long? Id)
{
public bool IsWaarschuwing => Soort == "overlijden";
public bool IsMoment => Soort == "moment";
}
/// <summary>Alles wat het beginscherm laat zien.</summary>
public record Overzicht(
int AantalKluis,
int AantalFamilie,
int AantalVoorMij,
int AantalVanMij,
int AantalNieuweBerichten,
int AantalBeheerders,
int VooruitblikDagen,
IReadOnlyList<Overzichtsregel> Regels);
/// <summary>
/// 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.
/// </summary>
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
};
}
/// <summary>
/// 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.
/// </summary>
public static class Datums
{
public static readonly string[] Maanden =
{
"januari", "februari", "maart", "april", "mei", "juni",
"juli", "augustus", "september", "oktober", "november", "december"
};
/// <summary>Afkortingen voor waar het krap is. "Mei" heeft geen punt: dat is geen afkorting.</summary>
public static readonly string[] KorteMaanden =
{
"Jan.", "Feb.", "Mrt.", "Apr.", "Mei", "Jun.",
"Jul.", "Aug.", "Sep.", "Okt.", "Nov.", "Dec."
};
/// <summary>Zondag eerst, want zo telt DayOfWeek.</summary>
public static readonly string[] Dagen =
{
"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"
};
}
/// <summary>
/// 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.
///
/// <see cref="Keuzes"/> is wat je kunt kiezen, <see cref="Naam"/> 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.
/// </summary>
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")
};
/// <summary>Hoe een relatie op het scherm heet - ook de relaties die je niet meer kunt kiezen.</summary>
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"
};
}
/// <summary>
/// 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.
/// </summary>
public record Aantekening(long PersoonId, string Van, string? Relatie, DateTime AangemaaktOp);
/// <summary>Eigendomsstatus van een profiel: 0 beheerd, 1 uitgenodigd, 2 geclaimd.</summary>
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;
/// <summary>De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.</summary>
public string? Profielfotobron =>
string.IsNullOrEmpty(Profielfoto) ? null : $"data:image/jpeg;base64,{Profielfoto}";
}
/// <summary>
/// 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.
/// </summary>
public record Opheffing(int Kluisitems, int Bestanden, int BerichtenKlaar, int BerichtenBezorgd,
int Mensen, int Nalatenschappen);
/// <summary>Je eigen gegevens, zoals het profielscherm ze toont.</summary>
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)));
/// <summary>De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.</summary>
public string? Profielfotobron =>
string.IsNullOrEmpty(Profielfoto) ? null : $"data:image/jpeg;base64,{Profielfoto}";
}
/// <summary>
/// Een nalatenschap die jij beheert. <see cref="Afgerond"/> is false zolang de persoon er
/// gewoon is: dan weet je alleen dat je bent aangewezen en valt er niets in te zien.
/// </summary>
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)
{
/// <summary>De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.</summary>
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);
/// <summary>De ontsleutelde inhoud van een kluisitem. Per soort is maar een deel gevuld.</summary>
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; }
/// <summary>Welk sociaal medium; een van SocialeMedia.Namen, of "Anders".</summary>
public string? Platform { get; set; }
/// <summary>Alleen gevuld als Platform "Anders" is.</summary>
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; }
}
/// <summary>Een bijlage bij een kluisitem, zonder de inhoud zelf.</summary>
public record Bijlage(long Id, string FileName, string ContentType, long SizeBytes, DateTime CreatedAt,
bool IsProfilePhoto = false)
{
/// <summary>De grootte zoals je hem aan iemand laat zien: "1,4 MB".</summary>
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);
}
/// <summary>Een opgehaald bestand, met de inhoud erbij.</summary>
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
/// <summary>0 tekst, 1 video, 2 audio, 3 document.</summary>
public bool IsVideo => Kind == 1;
/// <summary>Zit er een opname of bijlage bij dit bericht?</summary>
public bool HeeftBestand => VaultFileId is > 0;
}
/// <summary>De uitkomst van een proefmelding: is hij verstuurd, en zo niet waarom niet.</summary>
public record Proefmelding(bool Gelukt, string Uitleg);
/// <summary>Iemand die jouw nalatenschap mag beheren. Volgorde 1 is de eerste keus.</summary>
/// <summary>
/// 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.
/// </summary>
/// <param name="Link">
/// 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.
/// </param>
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;
/// <summary>De kleine profielfoto als verwijzing die een img-element rechtstreeks aankan.</summary>
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;
}