using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace Elternbeirat.PocketBase; /// /// Shared parsing of a PocketBase date string as wall-clock time. The trailing /// "Z" is stripped rather than honoured, so the number is taken at face value and /// the result carries -- no timezone shift. /// internal static class WallClock { public static DateTime Parse(string raw) { // Drop a trailing "Z" so DateTime.Parse does not treat the value as UTC // and convert it to local time (which would shift 19:30 to 20:30/21:30). var value = raw.EndsWith('Z') ? raw[..^1] : raw; var parsed = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.None); return DateTime.SpecifyKind(parsed, DateTimeKind.Unspecified); } } /// /// Reads PocketBase date values as local wall-clock time. /// /// PocketBase stores every date in UTC and serializes it with a trailing "Z" /// (e.g. "2026-10-08 19:30:00.000Z"). By project convention the stored number /// IS the local time (Europe/Berlin) and the "Z" is ignored -- see the timezone /// decision in the data model. This converter therefore parses the value and /// returns it as an instant, so no /// timezone shift is ever applied when the value is later formatted. /// /// public sealed class LocalDateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var raw = reader.GetString(); return string.IsNullOrEmpty(raw) ? default : WallClock.Parse(raw); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture)); } /// /// Nullable counterpart of . PocketBase sends /// an empty string for an unset optional date (e.g. an event without an end); /// that maps to null. /// public sealed class NullableLocalDateTimeConverter : JsonConverter { public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var raw = reader.GetString(); if (string.IsNullOrEmpty(raw)) return null; return WallClock.Parse(raw); } public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { if (value is null) writer.WriteStringValue(""); else writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture)); } }