Enhance scheduling and trace functionalities: add CollectionId to ScheduleEntry and PlannedItem models, update related query handlers and frontend components to support collection information in entry traces. Improve data handling in scheduling logic and enhance user experience in trace display.
This commit is contained in:
@@ -142,7 +142,8 @@ public sealed class GridScheduleGenerator(
|
||||
item.SlotId,
|
||||
item.Trace is null
|
||||
? null
|
||||
: JsonSerializer.Serialize(item.Trace, TraceJsonOptions)
|
||||
: JsonSerializer.Serialize(item.Trace, TraceJsonOptions),
|
||||
item.CollectionId
|
||||
)
|
||||
);
|
||||
added++;
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Domain.Programming.Planning;
|
||||
|
||||
namespace TeleWave.Application.Programming.Planning.Trace;
|
||||
|
||||
/// <summary>
|
||||
/// Цепочка происхождения записи — «почему это здесь» (см. 6.5). Трейс пишется в момент генерации
|
||||
/// и хранится в самой записи: восстановить его потом невозможно, состав групп и правила меняются.
|
||||
/// </summary>
|
||||
public sealed record GetEntryTraceQuery(Guid EntryId) : IQuery<Result<EntryTraceDto>>;
|
||||
|
||||
public sealed record EntryTraceDto(
|
||||
Guid EntryId,
|
||||
DateTimeOffset StartsAtUtc,
|
||||
DateTimeOffset EndsAtUtc,
|
||||
string? ShowName,
|
||||
int? EpisodeIndex,
|
||||
/// <summary>Слой и слот, из которых выросла запись; null — трейс не писался (старая запись).</summary>
|
||||
string? LayerName,
|
||||
int? LayerPriority,
|
||||
string? SlotTitle,
|
||||
SlotKind? SlotKind,
|
||||
int? SlotWeekday,
|
||||
TimeOnly? SlotTargetStart,
|
||||
int? SlotDurationMinutes,
|
||||
string? GroupName,
|
||||
int? GroupItemCount,
|
||||
SlotStrategyKind? Strategy,
|
||||
int? CooldownDays,
|
||||
/// <summary>Сколько кандидатов осталось после остывания (null — выбор шёл без него).</summary>
|
||||
int? CandidatesAfterCooldown,
|
||||
int DriftMinutes,
|
||||
bool Snapped,
|
||||
string? JunctionName
|
||||
);
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Domain.Programming.Planning;
|
||||
|
||||
namespace TeleWave.Application.Programming.Planning.Trace;
|
||||
|
||||
/// <summary>
|
||||
/// Цепочка происхождения записи — «почему это здесь» (см. 6.5). Трейс пишется в момент генерации
|
||||
/// и хранится в самой записи: восстановить его потом невозможно, состав групп и правила меняются.
|
||||
/// </summary>
|
||||
public sealed record GetEntryTraceQuery(Guid EntryId) : IQuery<Result<EntryTraceDto>>;
|
||||
|
||||
public sealed record EntryTraceDto(
|
||||
Guid EntryId,
|
||||
DateTimeOffset StartsAtUtc,
|
||||
DateTimeOffset EndsAtUtc,
|
||||
string? ShowName,
|
||||
int? EpisodeIndex,
|
||||
/// <summary>Слой и слот, из которых выросла запись; null — трейс не писался (старая запись).</summary>
|
||||
string? LayerName,
|
||||
int? LayerPriority,
|
||||
string? SlotTitle,
|
||||
SlotKind? SlotKind,
|
||||
int? SlotWeekday,
|
||||
TimeOnly? SlotTargetStart,
|
||||
int? SlotDurationMinutes,
|
||||
string? GroupName,
|
||||
int? GroupItemCount,
|
||||
/// <summary>Коллекция, частью которой шла запись, — если в эфир шла франшиза, а не одиночное шоу.</summary>
|
||||
string? CollectionName,
|
||||
SlotStrategyKind? Strategy,
|
||||
int? CooldownDays,
|
||||
/// <summary>Сколько кандидатов осталось после остывания (null — выбор шёл без него).</summary>
|
||||
int? CandidatesAfterCooldown,
|
||||
int DriftMinutes,
|
||||
bool Snapped,
|
||||
string? JunctionName
|
||||
);
|
||||
|
||||
+151
-141
@@ -1,141 +1,151 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Domain.Programming.Planning;
|
||||
|
||||
namespace TeleWave.Application.Programming.Planning.Trace;
|
||||
|
||||
public sealed class GetEntryTraceQueryHandler(IAppDbContext dbContext)
|
||||
: IQueryHandler<GetEntryTraceQuery, Result<EntryTraceDto>>
|
||||
{
|
||||
/// <summary>Те же настройки, что при записи трейса генератором.</summary>
|
||||
private static readonly JsonSerializerOptions TraceJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
public async Task<Result<EntryTraceDto>> Handle(
|
||||
GetEntryTraceQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var entry = await dbContext
|
||||
.ScheduleEntries.AsNoTracking()
|
||||
.FirstOrDefaultAsync(e => e.Id == query.EntryId, cancellationToken);
|
||||
if (entry is null)
|
||||
return Result.Failure<EntryTraceDto>(ChannelErrors.NotFound);
|
||||
|
||||
var showName =
|
||||
entry.ShowId is { } showId
|
||||
? await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => s.Id == showId)
|
||||
.Select(s => s.Name)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
var trace = Parse(entry.TraceJson);
|
||||
if (trace is null)
|
||||
return Result.Success(
|
||||
new EntryTraceDto(
|
||||
entry.Id,
|
||||
entry.StartsAtUtc,
|
||||
entry.EndsAtUtc,
|
||||
showName,
|
||||
entry.EpisodeIndex,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
false,
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
// Слот мог быть удалён или изменён после генерации — трейс от этого не портится, просто
|
||||
// часть подписей окажется пустой.
|
||||
var slot = trace.SlotId is { } slotId
|
||||
? await dbContext.Slots.AsNoTracking().FirstOrDefaultAsync(s => s.Id == slotId, cancellationToken)
|
||||
: null;
|
||||
var layer = slot is null
|
||||
? null
|
||||
: await dbContext
|
||||
.GridLayers.AsNoTracking()
|
||||
.FirstOrDefaultAsync(l => l.Id == slot.LayerId, cancellationToken);
|
||||
|
||||
var group = slot?.GroupId is { } groupId
|
||||
? await dbContext
|
||||
.Groups.AsNoTracking()
|
||||
.Where(g => g.Id == groupId)
|
||||
.Select(g => new { g.Name, g.ItemCount })
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
var strategy = SlotStrategy.FromJson(slot?.StrategyJson);
|
||||
|
||||
var junctionId = slot?.JunctionAfterId ?? slot?.JunctionBetweenId;
|
||||
var junctionName = junctionId is { } id
|
||||
? await dbContext
|
||||
.JunctionTemplates.AsNoTracking()
|
||||
.Where(j => j.Id == id)
|
||||
.Select(j => j.Name)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
return Result.Success(
|
||||
new EntryTraceDto(
|
||||
entry.Id,
|
||||
entry.StartsAtUtc,
|
||||
entry.EndsAtUtc,
|
||||
showName,
|
||||
entry.EpisodeIndex,
|
||||
layer?.Name,
|
||||
layer?.Priority,
|
||||
slot?.Title,
|
||||
trace.SlotKind,
|
||||
slot?.Weekday,
|
||||
slot?.TargetStart,
|
||||
slot?.TargetDurationMinutes,
|
||||
group?.Name,
|
||||
group?.ItemCount,
|
||||
trace.Strategy,
|
||||
strategy?.CooldownDays,
|
||||
trace.CandidatesAfterCooldown,
|
||||
trace.DriftMinutes,
|
||||
trace.Snapped,
|
||||
junctionName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static PlanTrace? Parse(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<PlanTrace>(json, TraceJsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Domain.Programming.Planning;
|
||||
|
||||
namespace TeleWave.Application.Programming.Planning.Trace;
|
||||
|
||||
public sealed class GetEntryTraceQueryHandler(IAppDbContext dbContext)
|
||||
: IQueryHandler<GetEntryTraceQuery, Result<EntryTraceDto>>
|
||||
{
|
||||
/// <summary>Те же настройки, что при записи трейса генератором.</summary>
|
||||
private static readonly JsonSerializerOptions TraceJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
public async Task<Result<EntryTraceDto>> Handle(
|
||||
GetEntryTraceQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var entry = await dbContext
|
||||
.ScheduleEntries.AsNoTracking()
|
||||
.FirstOrDefaultAsync(e => e.Id == query.EntryId, cancellationToken);
|
||||
if (entry is null)
|
||||
return Result.Failure<EntryTraceDto>(ChannelErrors.NotFound);
|
||||
|
||||
var showName =
|
||||
entry.ShowId is { } showId
|
||||
? await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => s.Id == showId)
|
||||
.Select(s => s.Name)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
var collectionName = entry.CollectionId is { } collectionId
|
||||
? await dbContext
|
||||
.Collections.AsNoTracking()
|
||||
.Where(c => c.Id == collectionId)
|
||||
.Select(c => c.Name)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
var trace = Parse(entry.TraceJson);
|
||||
if (trace is null)
|
||||
return Result.Success(
|
||||
new EntryTraceDto(
|
||||
entry.Id,
|
||||
entry.StartsAtUtc,
|
||||
entry.EndsAtUtc,
|
||||
showName,
|
||||
entry.EpisodeIndex,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
collectionName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
false,
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
// Слот мог быть удалён или изменён после генерации — трейс от этого не портится, просто
|
||||
// часть подписей окажется пустой.
|
||||
var slot = trace.SlotId is { } slotId
|
||||
? await dbContext.Slots.AsNoTracking().FirstOrDefaultAsync(s => s.Id == slotId, cancellationToken)
|
||||
: null;
|
||||
var layer = slot is null
|
||||
? null
|
||||
: await dbContext
|
||||
.GridLayers.AsNoTracking()
|
||||
.FirstOrDefaultAsync(l => l.Id == slot.LayerId, cancellationToken);
|
||||
|
||||
var group = slot?.GroupId is { } groupId
|
||||
? await dbContext
|
||||
.Groups.AsNoTracking()
|
||||
.Where(g => g.Id == groupId)
|
||||
.Select(g => new { g.Name, g.ItemCount })
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
var strategy = SlotStrategy.FromJson(slot?.StrategyJson);
|
||||
|
||||
var junctionId = slot?.JunctionAfterId ?? slot?.JunctionBetweenId;
|
||||
var junctionName = junctionId is { } id
|
||||
? await dbContext
|
||||
.JunctionTemplates.AsNoTracking()
|
||||
.Where(j => j.Id == id)
|
||||
.Select(j => j.Name)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
: null;
|
||||
|
||||
return Result.Success(
|
||||
new EntryTraceDto(
|
||||
entry.Id,
|
||||
entry.StartsAtUtc,
|
||||
entry.EndsAtUtc,
|
||||
showName,
|
||||
entry.EpisodeIndex,
|
||||
layer?.Name,
|
||||
layer?.Priority,
|
||||
slot?.Title,
|
||||
trace.SlotKind,
|
||||
slot?.Weekday,
|
||||
slot?.TargetStart,
|
||||
slot?.TargetDurationMinutes,
|
||||
group?.Name,
|
||||
group?.ItemCount,
|
||||
collectionName,
|
||||
trace.Strategy,
|
||||
strategy?.CooldownDays,
|
||||
trace.CandidatesAfterCooldown,
|
||||
trace.DriftMinutes,
|
||||
trace.Snapped,
|
||||
junctionName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static PlanTrace? Parse(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<PlanTrace>(json, TraceJsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user