Files
TeleWave/backend/src/TeleWave.Domain/Programming/ScheduleTemplate.cs
T

106 lines
4.9 KiB
C#

namespace TeleWave.Domain.Programming;
/// <summary>
/// Шаблон сетки канала: слои со слотами плюс аварийные настройки. Один активный шаблон на канал —
/// сезонность выражается слоями внутри него, а не вторым шаблоном, иначе получились бы два механизма
/// для одного и того же. На другой канал переносится глубокой копией.
///
/// <see cref="Revision"/> растёт при любой правке правил. Эфир при этом не меняется: правка помечает
/// шаблон изменённым, а хвост пересобирается отдельной командой применения.
/// </summary>
public class ScheduleTemplate
{
private readonly List<GridLayer> _layers = [];
public Guid Id { get; private set; }
public Guid ChannelId { get; private set; }
public string Name { get; private set; } = string.Empty;
/// <summary>Аварийная группа, если пуст даже фоновый слой.</summary>
public Guid? FallbackGroupId { get; private set; }
/// <summary>Стык по умолчанию — для слотов, у которых свой не задан.</summary>
public Guid? DefaultJunctionId { get; private set; }
/// <summary>
/// Правила отбора кандидатов (детское время, потолок повторов) в JSON. Домен их не разбирает —
/// схема живёт в Application, как и у стратегий слотов и условий стыка.
/// </summary>
public string? RulesJson { get; private set; }
/// <summary>Номер правки правил; входит в кэш-ключи и историю.</summary>
public int Revision { get; private set; }
/// <summary>Ревизия, по которой собрано текущее будущее расписание.</summary>
public int AppliedRevision { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
public IReadOnlyList<GridLayer> Layers => _layers;
/// <summary>Есть ли правки, не применённые к эфиру.</summary>
public bool HasPendingChanges => Revision != AppliedRevision;
private const string BackgroundLayerName = "Фон";
private ScheduleTemplate() { }
public static ScheduleTemplate Create(Guid channelId, string name)
{
var template = new ScheduleTemplate
{
Id = Guid.NewGuid(),
ChannelId = channelId,
Name = name.Trim(),
Revision = 0,
AppliedRevision = 0,
CreatedAt = DateTimeOffset.UtcNow,
};
// Фоновый слой заводится сразу: без него первая же дыра в сетке осталась бы нечем закрыть.
template._layers.Add(
GridLayer.Create(template.Id, BackgroundLayerName, GridLayer.BackgroundPriority, isBackground: true)
);
return template;
}
public void Rename(string name) => Name = name.Trim();
public void SetFallbackGroup(Guid? groupId) => FallbackGroupId = groupId;
public void SetDefaultJunction(Guid? junctionId) => DefaultJunctionId = junctionId;
public void SetRules(string? rulesJson) =>
RulesJson = string.IsNullOrWhiteSpace(rulesJson) ? null : rulesJson;
/// <summary>Отметить, что правила изменились — эфир пойдёт по старым до применения.</summary>
public void MarkChanged() => Revision++;
/// <summary>Отметить, что хвост пересобран по текущей ревизии.</summary>
public void MarkApplied() => AppliedRevision = Revision;
public GridLayer? FindLayer(Guid layerId) => _layers.FirstOrDefault(l => l.Id == layerId);
public GridLayer? Background => _layers.FirstOrDefault(l => l.IsBackground);
public GridLayer AddLayer(string name, int priority)
{
var layer = GridLayer.Create(Id, name, priority);
_layers.Add(layer);
return layer;
}
/// <summary>Удаляет слой. Фоновый удалить нельзя — вернёт false.</summary>
public bool RemoveLayer(Guid layerId)
{
var layer = _layers.FirstOrDefault(l => l.Id == layerId);
if (layer is null || layer.IsBackground)
return false;
_layers.Remove(layer);
return true;
}
/// <summary>Слой, содержащий слот, или null.</summary>
public GridLayer? FindLayerOfSlot(Guid slotId) =>
_layers.FirstOrDefault(l => l.FindSlot(slotId) is not null);
}