Files
ersatztv/ErsatzTV/Pages/TemplateEditor.razor
T

283 lines
10 KiB
Plaintext

@page "/templates/{Id:int}"
@using System.Globalization
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<TemplateEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Edit Template</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px">
<MudCard>
<MudCardContent>
<MudTextField Label="Name" @bind-Value="_template.Name" For="@(() => _template.Name)"/>
</MudCardContent>
</MudCard>
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4">
Save Changes
</MudButton>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px">
<MudCard>
<MudCardContent>
<MudSelect T="BlockGroupViewModel"
Label="Block Group"
ValueChanged="@(vm => UpdateBlockGroupItems(vm))">
@foreach (BlockGroupViewModel blockGroup in _blockGroups)
{
<MudSelectItem Value="@blockGroup">@blockGroup.Name</MudSelectItem>
}
</MudSelect>
<MudSelect Class="mt-3"
T="BlockViewModel"
Label="Block"
@bind-value="_selectedBlock">
@foreach (BlockViewModel block in _blocks)
{
<MudSelectItem Value="@block">@block.Name</MudSelectItem>
}
</MudSelect>
<MudSelect Class="mt-3"
T="DateTime"
Label="Start Time On Or After"
@bind-value="_selectedBlockStart">
@foreach (DateTime startTime in _startTimes)
{
<MudSelectItem Value="@startTime">
@startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern)
</MudSelectItem>
}
</MudSelect>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlockToTemplate())" Disabled="@(_selectedBlock is null)">
Add Block To Template
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px">
<MudCard>
<MudCardContent>
<MudSelect T="TemplateItemEditViewModel"
Label="Block To Remove"
@bind-Value="_blockToRemove">
<MudSelectItem Value="@((TemplateItemEditViewModel)null)">(none)</MudSelectItem>
@foreach (TemplateItemEditViewModel item in _template.Items.OrderBy(i => i.Start))
{
<MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem>
}
</MudSelect>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveBlockFromTemplate())" Disabled="@(_blockToRemove is null)">
Remove Block From Template
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCalendar Class="mt-4"
Items="@_template.Items"
ShowMonth="false"
ShowWeek="false"
ShowPrevNextButtons="false"
ShowDatePicker="false"
ShowTodayButton="false"
DayTimeInterval="CalendarTimeInterval.Minutes10"
Use24HourClock="@(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern.Contains("H"))"
EnableDragItems="true"
EnableResizeItems="false"
ItemChanged="@(ci => CalendarItemChanged(ci))"/>
</MudItem>
</MudGrid>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private readonly List<BlockGroupViewModel> _blockGroups = [];
private readonly List<BlockViewModel> _blocks = [];
private readonly List<DateTime> _startTimes = [];
[Parameter]
public int Id { get; set; }
private TemplateItemsEditViewModel _template = new();
private TemplateItemEditViewModel _blockToRemove = new();
private BlockGroupViewModel _selectedBlockGroup;
private BlockViewModel _selectedBlock;
private DateTime _selectedBlockStart;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadTemplateItems();
DateTime start = DateTime.Today;
_selectedBlockStart = start;
while (start.Date == DateTime.Today.Date)
{
_startTimes.Add(start);
start = start.AddMinutes(5);
}
}
private async Task LoadTemplateItems()
{
Option<TemplateViewModel> maybeTemplate = await Mediator.Send(new GetTemplateById(Id), _cts.Token);
if (maybeTemplate.IsNone)
{
NavigationManager.NavigateTo("templates");
return;
}
foreach (TemplateViewModel template in maybeTemplate)
{
_template = new TemplateItemsEditViewModel { Name = template.Name };
}
Option<IEnumerable<TemplateItemViewModel>> maybeResults = await Mediator.Send(new GetTemplateItems(Id), _cts.Token);
foreach (IEnumerable<TemplateItemViewModel> items in maybeResults)
{
_template.Items.AddRange(items.Map(ProjectToEditViewModel));
}
_blockGroups.AddRange(await Mediator.Send(new GetAllBlockGroups(), _cts.Token));
}
private static TemplateItemEditViewModel ProjectToEditViewModel(TemplateItemViewModel item) =>
new()
{
BlockId = item.BlockId,
BlockName = item.BlockName,
Start = item.StartTime,
End = item.EndTime
};
private async Task UpdateBlockGroupItems(BlockGroupViewModel blockGroup)
{
_selectedBlockGroup = blockGroup;
_blocks.Clear();
_blocks.AddRange(await Mediator.Send(new GetBlocksByBlockGroupId(_selectedBlockGroup.Id), _cts.Token));
}
private void AddBlockToTemplate()
{
// find first time where this block will fit
DateTime maybeStart = _selectedBlockStart;
while (maybeStart.Date == DateTime.Today)
{
DateTime maybeEnd = maybeStart.AddMinutes(_selectedBlock.Minutes);
if (IntersectsOthers(null, maybeStart, maybeEnd) == false)
{
var item = new TemplateItemEditViewModel
{
BlockId = _selectedBlock.Id,
BlockName = _selectedBlock.Name,
Start = maybeStart,
End = maybeEnd,
LastStart = maybeStart,
LastEnd = maybeEnd
};
_template.Items.Add(item);
break;
}
maybeStart = maybeStart.AddMinutes(5);
}
}
private async Task RemoveBlockFromTemplate()
{
if (_blockToRemove is not null)
{
_template.Items.Remove(_blockToRemove);
_blockToRemove = null;
await InvokeAsync(StateHasChanged);
}
}
private void CalendarItemChanged(CalendarItem calendarItem)
{
// don't allow any overlap
if (calendarItem is TemplateItemEditViewModel item)
{
bool intersects = item.End.HasValue && IntersectsOthers(item, item.Start, item.End.Value);
bool crossesMidnight = item.End.HasValue && item.End.Value.TimeOfDay > TimeSpan.Zero && item.End.Value.TimeOfDay < item.Start.TimeOfDay;
if (intersects || crossesMidnight)
{
// roll back
item.Start = item.LastStart;
item.End = item.LastEnd;
}
else
{
// commit
item.LastStart = item.Start;
item.LastEnd = item.End;
}
}
}
private bool IntersectsOthers(TemplateItemEditViewModel item, DateTime start, DateTime end)
{
var willFit = true;
foreach (TemplateItemEditViewModel existing in _template.Items)
{
if (existing == item)
{
continue;
}
if (start < existing.End && existing.Start < end)
{
willFit = false;
break;
}
}
return willFit == false;
}
// private void RemoveTemplateItem(TemplateItemEditViewModel item)
// {
// _selectedItem = null;
// _template.Items.Remove(item);
// }
private async Task SaveChanges()
{
var items = _template.Items.Map(item => new ReplaceTemplateItem(item.BlockId, item.Start.TimeOfDay)).ToList();
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceTemplateItems(Id, _template.Name, items), _cts.Token)
.Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match(
error =>
{
Snackbar.Add($"Unexpected error saving template: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving template: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("templates"));
}
}