{ config, ... }:
{
config.types.event =
event@{ lib, events, ... }:
let
inherit (lib) mkOption types;
cfg = event.config;
sort-key = e: "${e.date.start} ${e.time.start}";
# https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Sakamoto's_methods
weekday =
date:
let
parts = map lib.toIntBase10 (lib.splitString "-" date);
m = lib.elemAt parts 1;
d = lib.elemAt parts 2;
y = lib.elemAt parts 0 - (if m < 3 then 1 else 0);
offset = lib.elemAt [ 0 3 2 5 0 3 5 1 4 6 2 4 ] (m - 1);
in
lib.elemAt [
"Sonntag"
"Montag"
"Dienstag"
"Mittwoch"
"Donnerstag"
"Freitag"
"Samstag"
] (lib.mod (y + y / 4 - y / 100 + y / 400 + offset + d) 7);
prior =
let
candidates = lib.filter (
e:
e.venue != null && e.venue == cfg.venue && sort-key e < sort-key cfg && !e.address.once
) (lib.attrValues events);
sorted = lib.sortOn sort-key candidates;
in
if sorted == [ ] then
throw "Kein vorheriger Termin mit gleichem Veranstaltungsort gefunden."
else
lib.last sorted;
in
{
imports = [ config.types.document ];
options = {
title = mkOption {
description = ''
Überschrift für den Termin
'';
type = types.singleLineStr;
};
summary = mkOption {
description = ''
Kurzbeschreibung des Termins
'';
type = with types; nullOr singleLineStr;
default = null;
};
aside = mkOption {
description = ''
Anmerkung zur Beschreibung
'';
type = with types; nullOr singleLineStr;
default = null;
};
description = mkOption {
description = ''
Beschreibung des Termins als Freitext
'';
type = with types; nullOr str;
default = null;
};
data = mkOption {
description = ''
Semistrukturierte Daten über den Termin
'';
type =
with types;
listOf (submodule {
options = {
name = mkOption {
type = singleLineStr;
};
value = mkOption {
type = singleLineStr;
};
};
});
example = [
{
name = "Eintritt";
value = "frei – um Spenden wird gebeten";
}
];
default = [ ];
};
links = mkOption {
description = ''
Links zu weiteren Informationen
'';
type =
with types;
listOf (submodule {
options = {
text = mkOption {
type = singleLineStr;
};
url = mkOption {
type = strMatching "^(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\\?([^#]*))?(#(.*))?";
};
external = mkOption {
type = bool;
default = true;
};
};
});
default = [ ];
example = {
text = "Details beim Veranstalter";
url = "https://examle.org/event/123";
};
};
hosts = mkOption {
description = ''
Veranstalter
'';
type =
with types;
listOf (submodule {
options = {
name = mkOption {
type = singleLineStr;
};
url = mkOption {
type = strMatching "^(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\\?([^#]*))?(#(.*))?";
};
};
});
default = [ ];
example = {
name = "Stadtteilbeirat Heimfeld";
url = "https://heimfeld.harburg";
};
};
date.start = mkOption {
description = ''
Datum des Termins
'';
type = types.strMatching "[0-9]{4}-[0-9]{2}-[0-9]{2}";
example = "2025-12-20";
};
date.end = mkOption {
description = ''
Enddatum des Termins
'';
type = with types; nullOr (strMatching "[0-9]{4}-[0-9]{2}-[0-9]{2}");
example = "2025-12-21";
default = null;
};
time.start = mkOption {
description = ''
Startzeit des Termins
'';
type = types.strMatching "[0-9]{2}:[0-9]{2}";
example = "17:00";
};
time.end = mkOption {
description = ''
Endzeit des Termins
'';
type = with types; nullOr (strMatching "[0-9]{2}:[0-9]{2}");
example = "19:00";
default = null;
};
datetime-range-text = mkOption {
description = ''
Zeitraum des Termins in Textform
'';
type = types.str;
example = "Samstag, 20.12.2025 von 15:00 bis 17:00 Uhr";
default =
let
date = lib.concatStringsSep "." (lib.reverseList (lib.splitString "-" cfg.date.start));
start = text: '''';
end = '''';
in
"${weekday cfg.date.start}, "
+ (
if cfg.time.end == null then
start "${date} ab ${cfg.time.start}"
else
"${start "${date} von ${cfg.time.start}"} bis ${end}"
);
defaultText = lib.literalMD "Wochentag, Datum und Uhrzeit aus den Zeitangaben des Termins";
};
venue = mkOption {
description = ''
Kennzeichen des Veranstaltungsorts zum Wiederverwenden von Angaben in nachfolgenden Terminen.
'';
type = with types; nullOr singleLineStr;
default = null;
};
address.name = mkOption {
description = ''
Eigenname des Veranstaltungsorts
'';
type = with types; nullOr str;
example = "Treffpunkthaus";
default = if cfg.venue == null then null else prior.address.name;
defaultText = lib.literalMD "Adresse des vorherigen Termins mit gleichem Veranstaltungsort, falls vorhanden.";
};
address.text = mkOption {
description = ''
Ort in Textform
'';
type = types.lines;
default =
if cfg.venue == null then
throw "Ohne Veranstaltungsort-Kennzeichen muss die Adresse vollständig angegeben werden."
else
prior.address.text;
defaultText = lib.literalMD "Adresse des vorherigen Termins mit gleichem Veranstaltungsort.";
};
address.openstreetmap = mkOption {
description = ''
Ort auf OpenStreetMap
'';
type =
with types;
attrTag {
way = mkOption {
type = ints.positive;
};
node = mkOption {
type = ints.positive;
};
};
example = {
node = 6549438363;
};
default =
if cfg.venue == null then
throw "Ohne Veranstaltungsort-Kennzeichen muss die Adresse vollständig angegeben werden."
else
prior.address.openstreetmap;
defaultText = lib.literalMD "Adresse des vorherigen Termins mit gleichem Veranstaltungsort.";
};
address.once = mkOption {
description = ''
Wenn gesetzt, wird die Adresse nicht in nachfolgende Termine vererbt.
'';
type = types.bool;
default = false;
};
};
};
}