using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Elternbeirat.PocketBase;
///
/// Converts between PocketBase date strings and Europe/Berlin wall-clock time.
///
///
/// PocketBase stores every date in UTC and serializes it with a trailing Z
/// (e.g. "2026-10-08 17:30:00.000Z"). The values produced here are local
/// wall-clock numbers: an editor who typed 19:30 in the admin form gets 19:30 back,
/// with daylight saving applied by the time zone. They carry
/// so that no later formatting shifts them again.
///
internal static class WallClock
{
///
/// IANA id; resolves on every platform .NET supports, Windows included.
///
private static readonly TimeZoneInfo Berlin =
TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
///
/// Parses a PocketBase UTC date string into Berlin wall-clock time.
///
///
/// The raw PocketBase value; may be or empty.
///
///
/// The Berlin wall-clock time with , or
/// if is or empty
/// (PocketBase's representation of an unset date).
///
///
/// is not a valid date string.
///
public static DateTime? Parse(string? raw) =>
string.IsNullOrEmpty(raw)
? null
: TimeZoneInfo.ConvertTime(
DateTimeOffset.Parse(raw, CultureInfo.InvariantCulture), Berlin).DateTime;
///
/// Formats a Berlin wall-clock time as the UTC string PocketBase stores.
/// Inverse of .
///
///
/// A Berlin wall-clock time. Its is ignored, and the
/// value is always interpreted as Berlin local time.
///
///
/// The UTC value in PocketBase format, e.g. "2026-10-08 17:30:00.000Z".
///
///
/// does not exist in Berlin, because it falls in the
/// daylight-saving gap in spring.
///
public static string Format(DateTime berlin) =>
TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(berlin, DateTimeKind.Unspecified), Berlin)
.ToString("yyyy-MM-dd HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
}
///
/// JSON converter that reads and writes PocketBase dates as Europe/Berlin
/// wall-clock values.
///
///
/// An empty string (unset date) is read as ().
/// For optional dates, use instead.
///
///
///
/// [JsonConverter(typeof(LocalDateTimeConverter))]
/// public DateTime Start { get; init; }
///
///
///
public sealed class LocalDateTimeConverter : JsonConverter
{
///
public override DateTime Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
=> WallClock.Parse(reader.GetString()) ?? default;
///
public override void Write(
Utf8JsonWriter writer,
DateTime value,
JsonSerializerOptions options)
=> writer.WriteStringValue(WallClock.Format(value));
}
///
/// JSON converter that reads and writes optional PocketBase dates as Europe/Berlin
/// wall-clock values.
///
///
/// PocketBase sends an empty string for an unset optional date (e.g. an event
/// without an end). This value, like JSON null, is read as .
///
///
///
/// [JsonConverter(typeof(NullableLocalDateTimeConverter))]
/// public DateTime? End { get; init; }
///
///
///
public sealed class NullableLocalDateTimeConverter : JsonConverter
{
///
public override DateTime? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
=> WallClock.Parse(reader.GetString());
///
///
/// Only called for non-null values: is
/// , so the serializer writes itself.
///
public override void Write(
Utf8JsonWriter writer,
DateTime? value,
JsonSerializerOptions options)
=> writer.WriteStringValue(WallClock.Format(value ?? throw new ArgumentNullException(nameof(value))));
}