diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-19 18:56:34 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-19 18:56:34 +0100 |
| commit | bee85bf92aab84087798ffa9f3f16336acef2fce (patch) | |
| tree | 4fc8e2907e6618940cd9bdeb3da1a81172aab459 /src/Enmarcha.Backend/Data/Models | |
| parent | fed5d57b9e5d3df7c34bccb7a120bfa274b2039a (diff) | |
Basic backoffice for alert management
Diffstat (limited to 'src/Enmarcha.Backend/Data/Models')
| -rw-r--r-- | src/Enmarcha.Backend/Data/Models/AlertSelector.cs | 19 | ||||
| -rw-r--r-- | src/Enmarcha.Backend/Data/Models/ServiceAlert.cs | 127 | ||||
| -rw-r--r-- | src/Enmarcha.Backend/Data/Models/TranslatedString.cs | 26 |
3 files changed, 172 insertions, 0 deletions
diff --git a/src/Enmarcha.Backend/Data/Models/AlertSelector.cs b/src/Enmarcha.Backend/Data/Models/AlertSelector.cs new file mode 100644 index 0000000..34b2de3 --- /dev/null +++ b/src/Enmarcha.Backend/Data/Models/AlertSelector.cs @@ -0,0 +1,19 @@ +namespace Enmarcha.Backend.Data.Models; + +/// <summary> +/// Defines the scope of an alert (e.g., "stop#vitrasa:1400", "route#xunta:123"). +/// This follows a URI-like pattern for easy parsing and matching. +/// </summary> +public class AlertSelector +{ + public string Raw { get; set; } = string.Empty; + + public string Type => Raw.Split('#').FirstOrDefault() ?? string.Empty; + public string Id => Raw.Split('#').ElementAtOrDefault(1) ?? string.Empty; + + public static AlertSelector FromStop(string feedId, string stopId) => new() { Raw = $"stop#{feedId}:{stopId}" }; + public static AlertSelector FromRoute(string feedId, string routeId) => new() { Raw = $"route#{feedId}:{routeId}" }; + public static AlertSelector FromAgency(string feedId) => new() { Raw = $"agency#{feedId}" }; + + public override string ToString() => Raw; +} diff --git a/src/Enmarcha.Backend/Data/Models/ServiceAlert.cs b/src/Enmarcha.Backend/Data/Models/ServiceAlert.cs new file mode 100644 index 0000000..5f80e3c --- /dev/null +++ b/src/Enmarcha.Backend/Data/Models/ServiceAlert.cs @@ -0,0 +1,127 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Enmarcha.Backend.Data.Models; + +[Table("service_alerts")] +public class ServiceAlert +{ + public string Id { get; set; } + + public List<AlertSelector> Selectors { get; set; } = []; + + public AlertCause Cause { get; set; } + public AlertEffect Effect { get; set; } + + public TranslatedString Header { get; set; } = []; + public TranslatedString Description { get; set; } = []; + [Column("info_urls")] public List<string> InfoUrls { get; set; } = []; + + [Column("inserted_date")] public DateTime InsertedDate { get; set; } + + [Column("publish_date")] public DateTime PublishDate { get; set; } + [Column("event_start_date")] public DateTime EventStartDate { get; set; } + [Column("event_end_date")] public DateTime EventEndDate { get; set; } + [Column("hidden_date")] public DateTime HiddenDate { get; set; } + + public AlertPhase GetPhase(DateTime? now = null) + { + now ??= DateTime.UtcNow; + + if (now < PublishDate) + { + return AlertPhase.Draft; + } + + if (now < EventStartDate) + { + return AlertPhase.PreNotice; + } + + if (now < EventEndDate) + { + return AlertPhase.Active; + } + + if (now < HiddenDate) + { + return AlertPhase.Finished; + } + + return AlertPhase.Done; + } +} + +/// <summary> +/// Phases of an alert lifecycle, not standard GTFS-RT, but useful if we can display a change to the service with a notice +/// before it actually starts affecting the service. For example, if we know that a strike will start on a certain date, we can show it as "PreNotice" +/// before it starts, then "Active" while it's happening, and "Finished" after it ends but before we hide it from the system, for example with +/// a checkmark saying "everything back to normal". +/// </summary> +public enum AlertPhase +{ + Draft = -1, + PreNotice = 0, + Active = 1, + Finished = 2, + Done = 3 +} + +public enum AlertCause +{ + [Description("Causa desconocida")] + UnknownCause = 1, + [Description("Otra causa")] + OtherCause = 2, // Not machine-representable. + [Description("Problema técnico")] + TechnicalProblem = 3, + [Description("Huelga (personal de la agencia)")] + Strike = 4, // Public transit agency employees stopped working. + [Description("Manifestación (otros)")] + Demonstration = 5, // People are blocking the streets. + [Description("Accidente")] + Accident = 6, + [Description("Festivo")] + Holiday = 7, + [Description("Condiciones meteorológicas")] + Weather = 8, + [Description("Obras en carretera (mantenimiento)")] + Maintenance = 9, + [Description("Obras próximas (construcción)")] + Construction = 10, + [Description("Intervención policial")] + PoliceActivity = 11, + [Description("Emergencia médica")] + MedicalEmergency = 12 +} + +public enum AlertEffect +{ + [Description("Sin servicio")] + NoService = 1, + [Description("Servicio reducido")] + ReducedService = 2, + + // We don't care about INsignificant delays: they are hard to detect, have + // little impact on the user, and would clutter the results as they are too + // frequent. + [Description("Retrasos significativos")] + SignificantDelays = 3, + + [Description("Desvío")] + Detour = 4, + [Description("Servicio adicional")] + AdditionalService = 5, + [Description("Servicio modificado")] + ModifiedService = 6, + [Description("Otro efecto")] + OtherEffect = 7, + [Description("Efecto desconocido")] + UnknownEffect = 8, + [Description("Parada movida")] + StopMoved = 9, + [Description("Sin efecto")] + NoEffect = 10, + [Description("Problemas de accesibilidad")] + AccessibilityIssue = 11 +} diff --git a/src/Enmarcha.Backend/Data/Models/TranslatedString.cs b/src/Enmarcha.Backend/Data/Models/TranslatedString.cs new file mode 100644 index 0000000..7bce8ea --- /dev/null +++ b/src/Enmarcha.Backend/Data/Models/TranslatedString.cs @@ -0,0 +1,26 @@ +namespace Enmarcha.Backend.Data.Models; + +/// <summary> +/// A translatable string that can be stored in the database as a single JSON column. +/// Keys are ISO language codes (e.g., "es", "gl", "en"). +/// </summary> +public class TranslatedString : Dictionary<string, string> +{ + public TranslatedString() : base() { } + + public TranslatedString(IDictionary<string, string> dictionary) : base(dictionary) { } + + /// <summary> + /// Gets the translation for the specified language, or a fallback if not found. + /// </summary> + public string Get(string lang, string fallback = "es") + { + if (TryGetValue(lang, out var value)) + return value; + + if (TryGetValue(fallback, out var fallbackValue)) + return fallbackValue; + + return Values.FirstOrDefault() ?? string.Empty; + } +} |
