Enhance GridPlanner and GenerateGridDialog for improved slot management and user experience
ci / build-backend (push) Successful in 2m30s
ci / build-frontend (push) Successful in 1m7s
ci / tests (push) Successful in 2m58s
ci / sonar (push) Successful in 6m28s

Refactored the GridPlanner class to optimize slot management during grid generation, ensuring more efficient handling of slots. Updated the GenerateGridDialog component to improve its layout and usability, enhancing the overall user experience when interacting with slot lists. These changes contribute to better functionality and accessibility in grid management.
This commit is contained in:
Leonid Pershin
2026-07-27 10:35:52 +03:00
parent b8d6b6aad2
commit 062f4aa200
8 changed files with 1197 additions and 0 deletions
@@ -0,0 +1,198 @@
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.GetTemplate;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using Xunit;
namespace TeleWave.Application.Tests.Programming;
/// <summary>
/// Сетка глазами редактора: слои по приоритету, слоты с именами групп и признаки состояния —
/// есть ли неприменённые правки и есть ли к чему откатываться.
/// </summary>
public class GetTemplateTests
{
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
[Fact]
public async Task ReturnsLayersByPriority_WithGroupNamesAndState()
{
var fixture = new TestDb();
var group = Group.Create("Кино");
var channel = Channel.Create("Первый", "first", T0);
channel.UpdateTimeSettings(1, 180, new TimeOnly(6, 0));
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
channel.SetTemplate(template.Id);
var top = template.AddLayer("Выходные", 20);
var slot = top.AddSlot("Кино в прайм", new TimeOnly(20, 0), 120, Daypart.Prime, weekday: 6);
slot.UpdateContent(
new SlotContent(
slot.Title,
SlotKind.Content,
group.Id,
new SlotStrategy(SlotStrategyType.RandomWithCooldown, CooldownDays: 14).ToJson(),
null,
SlotBlockMode.Count,
1,
OverflowPolicy.ContinueNext
)
);
template.MarkChanged();
await using (var seed = fixture.New())
{
seed.Groups.Add(group);
seed.Channels.Add(channel);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new GetChannelTemplateQueryHandler(db).Handle(
new GetChannelTemplateQuery(channel.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
var dto = result.Value;
// Слои идут сверху вниз: первым тот, что перекрывает остальные.
Assert.Equal(["Выходные", "Фон"], dto.Layers.Select(l => l.Name));
Assert.Equal(new TimeOnly(6, 0), dto.DayStartTime);
Assert.Equal(180, dto.UtcOffsetMinutes);
var stored = Assert.Single(dto.Layers[0].Slots);
// Имя группы приезжает вместе со слотом: инспектор показывает его без второго запроса.
Assert.Equal("Кино", stored.GroupName);
Assert.Equal(6, stored.Weekday);
Assert.Equal(SlotStrategyType.RandomWithCooldown, stored.Strategy!.Type);
Assert.Equal(14, stored.Strategy.CooldownDays);
// Правка эфир не двигает — она поднимает ревизию, и плашка об этом видна в шапке канала.
Assert.True(dto.HasPendingChanges);
Assert.Equal(1, dto.Revision);
Assert.Equal(0, dto.AppliedRevision);
// Сетку ни разу не применяли — откатывать не к чему, и кнопки быть не должно.
Assert.False(dto.CanRestore);
}
[Fact]
public async Task CanRestore_AfterFirstApply()
{
var fixture = new TestDb();
var channel = Channel.Create("Первый", "first", T0);
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
channel.SetTemplate(template.Id);
template.MarkApplied(TemplateSnapshot.Capture(template).ToJson());
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.ScheduleTemplates.Add(template);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var result = await new GetChannelTemplateQueryHandler(db).Handle(
new GetChannelTemplateQuery(channel.Id),
CancellationToken.None
);
Assert.True(result.Value.CanRestore);
Assert.False(result.Value.HasPendingChanges);
}
[Fact]
public async Task Fails_WhenChannelOrTemplateIsMissing()
{
var fixture = new TestDb();
var channel = Channel.Create("Без сетки", "no-grid", T0);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var handler = new GetChannelTemplateQueryHandler(db);
var noChannel = await handler.Handle(
new GetChannelTemplateQuery(Guid.NewGuid()),
CancellationToken.None
);
var noTemplate = await handler.Handle(
new GetChannelTemplateQuery(channel.Id),
CancellationToken.None
);
Assert.False(noChannel.IsSuccess);
Assert.False(noTemplate.IsSuccess);
}
[Fact]
public async Task ElementResolver_ExpandsCollectionIntoItsParts()
{
// Коллекция в группе — одна позиция, но эфира она даёт столько же, сколько все её части.
var fixture = new TestDb();
var film = Show.Create("Часть 1", ShowKind.Single);
film.SetAudience(ShowAudience.Pg13);
var sequel = Show.Create("Часть 2", ShowKind.Single);
sequel.SetAudience(ShowAudience.R);
var assets = new List<MediaAsset>();
foreach (var show in new[] { film, sequel })
{
var asset = MediaAsset.Register($"{show.Name}.mkv", ".mkv", MediaSource.Upload);
asset.MarkProcessing();
asset.MarkReady(
new MediaReadyInfo(
TimeSpan.FromMinutes(90),
6,
900,
1920,
1080,
"h264",
"aac",
$"assets/{show.Name}"
)
);
assets.Add(asset);
show.AddEpisode(asset.Id);
}
var collection = Collection.Create("Франшиза");
collection.AddShow(film.Id);
collection.AddShow(sequel.Id);
await using (var seed = fixture.New())
{
seed.MediaAssets.AddRange(assets);
seed.Shows.AddRange(film, sequel);
seed.Collections.Add(collection);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
var info = await new GroupElementResolver(db).ResolveAsync(
[(GroupElementKind.Collection, collection.Id), (GroupElementKind.Show, film.Id)],
CancellationToken.None
);
var franchise = info[(GroupElementKind.Collection, collection.Id)];
Assert.Equal("Франшиза", franchise.Name);
Assert.Equal(2, franchise.UnitCount);
Assert.Equal(TimeSpan.FromMinutes(180), franchise.TotalDuration);
// Рейтинг франшизы — самый строгий среди частей: по нему её отсеивают в детское время.
Assert.Equal(ShowAudience.R, franchise.Audience);
var single = info[(GroupElementKind.Show, film.Id)];
Assert.Equal(1, single.UnitCount);
Assert.Equal(ShowKind.Single, single.ShowKind);
}
}