mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
13 KiB
320 lines
13 KiB
@page "/deco-templates/{Id:int}" |
|
@using System.Globalization |
|
@using ErsatzTV.Application.Scheduling |
|
@implements IDisposable |
|
@inject NavigationManager NavigationManager |
|
@inject ILogger<DecoTemplateEditor> Logger |
|
@inject ISnackbar Snackbar |
|
@inject IMediator Mediator |
|
|
|
<MudForm Style="max-height: 100%"> |
|
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center"> |
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save"> |
|
Save Deco Template |
|
</MudButton> |
|
</MudPaper> |
|
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto"> |
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
|
<MudText Typo="Typo.h5" Class="mb-2">General</MudText> |
|
<MudDivider Class="mb-6"/> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Name</MudText> |
|
</div> |
|
<MudTextField @bind-Value="_decoTemplate.Name" For="@(() => _decoTemplate.Name)"/> |
|
</MudStack> |
|
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Add Content</MudText> |
|
<MudDivider Class="mb-6"/> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Deco Group</MudText> |
|
</div> |
|
<MudSelect T="DecoGroupViewModel" ValueChanged="@(vm => UpdateDecoGroupItems(vm))"> |
|
@foreach (DecoGroupViewModel decoGroup in _decoGroups) |
|
{ |
|
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem> |
|
} |
|
</MudSelect> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Deco</MudText> |
|
</div> |
|
<MudSelect T="DecoViewModel" @bind-value="_selectedDeco"> |
|
@foreach (DecoViewModel deco in _decos) |
|
{ |
|
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem> |
|
} |
|
</MudSelect> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Start Time On Or After</MudText> |
|
</div> |
|
<MudSelect T="DateTime" @bind-value="_selectedDecoStart"> |
|
@foreach (DateTime startTime in _startTimes) |
|
{ |
|
<MudSelectItem Value="@startTime"> |
|
@startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern) |
|
</MudSelectItem> |
|
} |
|
</MudSelect> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Duration</MudText> |
|
</div> |
|
<MudTextField T="int" |
|
@bind-Value="_durationHours" |
|
Adornment="Adornment.End" |
|
AdornmentText="hours"/> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"></div> |
|
<MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes"> |
|
<MudSelectItem Value="0"/> |
|
<MudSelectItem Value="5"/> |
|
<MudSelectItem Value="10"/> |
|
<MudSelectItem Value="15"/> |
|
<MudSelectItem Value="20"/> |
|
<MudSelectItem Value="25"/> |
|
<MudSelectItem Value="30"/> |
|
<MudSelectItem Value="35"/> |
|
<MudSelectItem Value="40"/> |
|
<MudSelectItem Value="45"/> |
|
<MudSelectItem Value="50"/> |
|
<MudSelectItem Value="55"/> |
|
</MudSelect> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"></div> |
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoToDecoTemplate())" Disabled="@(_selectedDeco is null)" StartIcon="@Icons.Material.Filled.Add"> |
|
Add Deco To Template |
|
</MudButton> |
|
</MudStack> |
|
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Remove Content</MudText> |
|
<MudDivider Class="mb-6"/> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Deco To Remove</MudText> |
|
</div> |
|
<MudSelect T="DecoTemplateItemEditViewModel" @bind-Value="_decoToRemove"> |
|
<MudSelectItem Value="@((DecoTemplateItemEditViewModel)null)">(none)</MudSelectItem> |
|
@foreach (DecoTemplateItemEditViewModel item in _decoTemplate.Items.OrderBy(i => i.Start)) |
|
{ |
|
<MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem> |
|
} |
|
</MudSelect> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"></div> |
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveDecoFromDecoTemplate())" Disabled="@(_decoToRemove is null)" StartIcon="@Icons.Material.Filled.Remove"> |
|
Remove Deco From Template |
|
</MudButton> |
|
</MudStack> |
|
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Content</MudText> |
|
<MudDivider Class="mb-6"/> |
|
<MudCalendar T="CalendarItem" |
|
Class="mb-6" |
|
Items="@_decoTemplate.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))"/> |
|
</MudContainer> |
|
</div> |
|
</MudForm> |
|
|
|
@code { |
|
private readonly CancellationTokenSource _cts = new(); |
|
private readonly List<DecoGroupViewModel> _decoGroups = []; |
|
private readonly List<DecoViewModel> _decos = []; |
|
private readonly List<DateTime> _startTimes = []; |
|
|
|
[Parameter] |
|
public int Id { get; set; } |
|
|
|
private DecoTemplateItemsEditViewModel _decoTemplate = new(); |
|
private DecoTemplateItemEditViewModel _decoToRemove; |
|
private DecoGroupViewModel _selectedDecoGroup; |
|
private DecoViewModel _selectedDeco; |
|
private DateTime _selectedDecoStart; |
|
private int _durationHours; |
|
private int _durationMinutes = 15; |
|
|
|
public void Dispose() |
|
{ |
|
_cts.Cancel(); |
|
_cts.Dispose(); |
|
} |
|
|
|
protected override async Task OnParametersSetAsync() |
|
{ |
|
await LoadDecoTemplateItems(); |
|
|
|
DateTime start = DateTime.Today; |
|
_selectedDecoStart = start; |
|
while (start.Date == DateTime.Today.Date) |
|
{ |
|
_startTimes.Add(start); |
|
start = start.AddMinutes(5); |
|
} |
|
} |
|
|
|
private async Task LoadDecoTemplateItems() |
|
{ |
|
Option<DecoTemplateViewModel> maybeDecoTemplate = await Mediator.Send(new GetDecoTemplateById(Id), _cts.Token); |
|
if (maybeDecoTemplate.IsNone) |
|
{ |
|
NavigationManager.NavigateTo("deco-templates"); |
|
return; |
|
} |
|
|
|
foreach (DecoTemplateViewModel template in maybeDecoTemplate) |
|
{ |
|
_decoTemplate = new DecoTemplateItemsEditViewModel { Name = template.Name }; |
|
} |
|
|
|
Option<IEnumerable<DecoTemplateItemViewModel>> maybeResults = await Mediator.Send(new GetDecoTemplateItems(Id), _cts.Token); |
|
foreach (IEnumerable<DecoTemplateItemViewModel> items in maybeResults) |
|
{ |
|
_decoTemplate.Items.AddRange(items.Map(ProjectToEditViewModel)); |
|
} |
|
|
|
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token)); |
|
} |
|
|
|
private static DecoTemplateItemEditViewModel ProjectToEditViewModel(DecoTemplateItemViewModel item) => |
|
new() |
|
{ |
|
DecoId = item.DecoId, |
|
DecoName = item.DecoName, |
|
Start = item.StartTime, |
|
End = item.EndTime |
|
}; |
|
|
|
private async Task UpdateDecoGroupItems(DecoGroupViewModel decoGroup) |
|
{ |
|
_selectedDecoGroup = decoGroup; |
|
|
|
_decos.Clear(); |
|
_decos.AddRange(await Mediator.Send(new GetDecosByDecoGroupId(_selectedDecoGroup.Id), _cts.Token)); |
|
} |
|
|
|
private void AddDecoToDecoTemplate() |
|
{ |
|
// find first time where this deco will fit |
|
DateTime maybeStart = _selectedDecoStart; |
|
while (maybeStart.Date == DateTime.Today) |
|
{ |
|
DateTime maybeEnd = maybeStart.AddHours(_durationHours).AddMinutes(_durationMinutes); |
|
if (IntersectsOthers(null, maybeStart, maybeEnd) == false) |
|
{ |
|
var item = new DecoTemplateItemEditViewModel |
|
{ |
|
DecoId = _selectedDeco.Id, |
|
DecoName = _selectedDeco.Name, |
|
Start = maybeStart, |
|
End = maybeEnd, |
|
LastStart = maybeStart, |
|
LastEnd = maybeEnd |
|
}; |
|
|
|
_decoTemplate.Items.Add(item); |
|
|
|
break; |
|
} |
|
|
|
maybeStart = maybeStart.AddMinutes(5); |
|
} |
|
} |
|
|
|
private async Task RemoveDecoFromDecoTemplate() |
|
{ |
|
if (_decoToRemove is not null) |
|
{ |
|
_decoTemplate.Items.Remove(_decoToRemove); |
|
_decoToRemove = null; |
|
|
|
await InvokeAsync(StateHasChanged); |
|
} |
|
} |
|
|
|
private void CalendarItemChanged(CalendarItem calendarItem) |
|
{ |
|
// don't allow any overlap |
|
if (calendarItem is DecoTemplateItemEditViewModel 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(DecoTemplateItemEditViewModel item, DateTime start, DateTime end) |
|
{ |
|
var willFit = true; |
|
|
|
foreach (DecoTemplateItemEditViewModel existing in _decoTemplate.Items) |
|
{ |
|
if (existing == item) |
|
{ |
|
continue; |
|
} |
|
|
|
if (start < existing.End && existing.Start < end) |
|
{ |
|
willFit = false; |
|
break; |
|
} |
|
} |
|
|
|
return willFit == false; |
|
} |
|
|
|
// private void RemoveDecoTemplateItem(DecoTemplateItemEditViewModel item) |
|
// { |
|
// _selectedItem = null; |
|
// _decoTemplate.Items.Remove(item); |
|
// } |
|
|
|
private async Task SaveChanges() |
|
{ |
|
if (_decoTemplate.Items.Any(i => i.End is null)) |
|
{ |
|
Snackbar.Add("Template item cannot end after midnight", Severity.Error); |
|
return; |
|
} |
|
|
|
var items = _decoTemplate.Items.Map(item => new ReplaceDecoTemplateItem(item.DecoId, item.Start.TimeOfDay, item.End!.Value.TimeOfDay)).ToList(); |
|
|
|
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceDecoTemplateItems(Id, _decoTemplate.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("deco-templates")); |
|
} |
|
|
|
} |