* add chapter statistics and new filler options * refactor playout builder * more refactor prep for filler * rewrite schedulers * refactor collectionkey * add tail filler kind * migrate tail filler to filler preset * optionally show filler * fix playout detail row count * remove duration tail filler options * implement tail and fallback in flood scheduler * implement tail and fallback in one scheduler * implement tail and fallback in multiple scheduler * implement looping fallback filler * more duration tests * start to add post-roll filler to flood * rework playoutitem filler tagging * rework scheduler logging * calculate whether configured filler will fit * implement pre-roll and post-roll duration and count filler * improve duration filler calculation * add minutes to search index * update channel guide to work with new filler * add mid-roll filler * don't clone enumerators for filler calculations * support pre-roll and post-roll pad filler * implement mid-roll pad filler * allow clearing filler selections in schedule editor * fix tests * filler config validation * use consistent time zone for tests
438 lines
24 KiB
Plaintext
438 lines
24 KiB
Plaintext
@page "/schedules/{Id:int}/items"
|
|
@using ErsatzTV.Application.MediaCollections
|
|
@using ErsatzTV.Application.MediaCollections.Queries
|
|
@using ErsatzTV.Application.MediaItems
|
|
@using ErsatzTV.Application.ProgramSchedules
|
|
@using ErsatzTV.Application.ProgramSchedules.Commands
|
|
@using ErsatzTV.Application.ProgramSchedules.Queries
|
|
@using ErsatzTV.Application.Television.Queries
|
|
@using ErsatzTV.Application.Artists.Queries
|
|
@using ErsatzTV.Application.Filler
|
|
@using ErsatzTV.Application.Filler.Queries
|
|
@using ErsatzTV.Core.Domain.Filler
|
|
@inject NavigationManager _navigationManager
|
|
@inject ILogger<ScheduleItemsEditor> _logger
|
|
@inject ISnackbar _snackbar
|
|
@inject IMediator _mediator
|
|
|
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
|
<MudTable Hover="true" Items="_schedule.Items.OrderBy(i => i.Index)" Dense="true" @bind-SelectedItem="_selectedItem">
|
|
<ToolBarContent>
|
|
<MudText Typo="Typo.h6">@_schedule.Name Items</MudText>
|
|
</ToolBarContent>
|
|
<ColGroup>
|
|
<col/>
|
|
<col/>
|
|
<col/>
|
|
<col style="width: 60px;"/>
|
|
<col style="width: 60px;"/>
|
|
<col style="width: 60px;"/>
|
|
</ColGroup>
|
|
<HeaderContent>
|
|
<MudTh>Start Time</MudTh>
|
|
<MudTh>Collection</MudTh>
|
|
<MudTh>Playout Mode</MudTh>
|
|
<MudTh/>
|
|
<MudTh/>
|
|
<MudTh/>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Start Time">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@(context.StartType == StartType.Fixed ? context.StartTime == null ? string.Empty : DateTime.Today.Add(context.StartTime.Value).ToShortTimeString() : "Dynamic")
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd DataLabel="Collection">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@context.CollectionName
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd DataLabel="Playout Mode">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@context.PlayoutMode
|
|
@if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue)
|
|
{
|
|
@($" ({context.MultipleCount})")
|
|
}
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
|
|
OnClick="@(_ => MoveItemUp(context))"
|
|
Disabled="@(_schedule.Items.All(x => x.Index >= context.Index))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
|
|
OnClick="@(_ => MoveItemDown(context))"
|
|
Disabled="@(_schedule.Items.All(x => x.Index <= context.Index))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
OnClick="@(_ => RemoveScheduleItem(context))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddScheduleItem())" Class="mt-4">
|
|
Add Schedule Item
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
|
|
Save Changes
|
|
</MudButton>
|
|
|
|
@if (_selectedItem is not null)
|
|
{
|
|
<EditForm Model="_selectedItem">
|
|
<FluentValidator/>
|
|
<div style="display: flex; flex-direction: row;" class="mt-6">
|
|
<div style="flex-grow: 1; max-width: 400px;" class="mr-6">
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudSelect Label="Start Type" @bind-Value="_selectedItem.StartType" For="@(() => _selectedItem.StartType)">
|
|
@foreach (StartType startType in Enum.GetValues<StartType>())
|
|
{
|
|
<MudSelectItem Value="@startType">@startType</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
|
|
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
|
|
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
|
|
{
|
|
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="MediaCollectionViewModel"
|
|
Label="Collection"
|
|
@bind-value="_selectedItem.Collection"
|
|
SearchFunc="@SearchMediaCollections"
|
|
ToStringFunc="@(c => c?.Name)"/>
|
|
}
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="MultiCollectionViewModel"
|
|
Label="Multi Collection"
|
|
@bind-value="_selectedItem.MultiCollection"
|
|
SearchFunc="@SearchMultiCollections"
|
|
ToStringFunc="@(c => c?.Name)"/>
|
|
}
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="SmartCollectionViewModel"
|
|
Label="Smart Collection"
|
|
@bind-value="_selectedItem.SmartCollection"
|
|
SearchFunc="@SearchSmartCollections"
|
|
ToStringFunc="@(c => c?.Name)"/>
|
|
}
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="NamedMediaItemViewModel"
|
|
Label="Television Show"
|
|
@bind-value="_selectedItem.MediaItem"
|
|
SearchFunc="@SearchTelevisionShows"
|
|
ToStringFunc="@(s => s?.Name)"/>
|
|
}
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="NamedMediaItemViewModel"
|
|
Label="Television Season"
|
|
@bind-value="_selectedItem.MediaItem"
|
|
SearchFunc="@SearchTelevisionSeasons"
|
|
ToStringFunc="@(s => s?.Name)"/>
|
|
}
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="NamedMediaItemViewModel"
|
|
Label="Artist"
|
|
@bind-value="_selectedItem.MediaItem"
|
|
SearchFunc="@SearchArtists"
|
|
ToStringFunc="@(s => s?.Name)"/>
|
|
}
|
|
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
|
|
@switch (_selectedItem.CollectionType)
|
|
{
|
|
case ProgramScheduleItemCollectionType.MultiCollection:
|
|
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>
|
|
break;
|
|
case ProgramScheduleItemCollectionType.Collection:
|
|
case ProgramScheduleItemCollectionType.SmartCollection:
|
|
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem>
|
|
break;
|
|
default:
|
|
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
|
break;
|
|
}
|
|
</MudSelect>
|
|
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
|
|
@foreach (PlayoutMode playoutMode in Enum.GetValues<PlayoutMode>())
|
|
{
|
|
<MudSelectItem Value="@playoutMode">@playoutMode</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
<MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/>
|
|
<MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
|
<MudSelect Class="mt-3" Label="Tail Mode" @bind-Value="@_selectedItem.TailMode" For="@(() => _selectedItem.TailMode)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)">
|
|
<MudSelectItem Value="@TailMode.None">(none)</MudSelectItem>
|
|
<MudSelectItem Value="@TailMode.Offline">Offline</MudSelectItem>
|
|
<MudSelectItem Value="@TailMode.Filler">Filler</MudSelectItem>
|
|
</MudSelect>
|
|
<MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/>
|
|
<MudSelect Class="mt-3" Label="Guide Mode" @bind-Value="@_selectedItem.GuideMode" For="@(() => _selectedItem.GuideMode)">
|
|
<MudSelectItem Value="@GuideMode.Normal">Normal</MudSelectItem>
|
|
<MudSelectItem Value="@GuideMode.Filler">Filler</MudSelectItem>
|
|
</MudSelect>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</div>
|
|
<div style="flex-grow: 1; max-width: 400px;">
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudAutocomplete Class="mt-3"
|
|
T="FillerPresetViewModel"
|
|
Label="Pre-Roll Filler"
|
|
@bind-value="_selectedItem.PreRollFiller"
|
|
SearchFunc="@(arg => SearchFillerPresets(FillerKind.PreRoll, arg))"
|
|
ToStringFunc="@(c => c?.Name)"
|
|
ResetValueOnEmptyText="true"
|
|
Clearable="true"/>
|
|
<MudAutocomplete Class="mt-3"
|
|
T="FillerPresetViewModel"
|
|
Label="Mid-Roll Filler"
|
|
@bind-value="_selectedItem.MidRollFiller"
|
|
SearchFunc="@(arg => SearchFillerPresets(FillerKind.MidRoll, arg))"
|
|
ToStringFunc="@(c => c?.Name)"
|
|
ResetValueOnEmptyText="true"
|
|
Clearable="true"/>
|
|
<MudAutocomplete Class="mt-3"
|
|
T="FillerPresetViewModel"
|
|
Label="Post-Roll Filler"
|
|
@bind-value="_selectedItem.PostRollFiller"
|
|
SearchFunc="@(arg => SearchFillerPresets(FillerKind.PostRoll, arg))"
|
|
ToStringFunc="@(c => c?.Name)"
|
|
ResetValueOnEmptyText="true"
|
|
Clearable="true"/>
|
|
<MudAutocomplete Class="mt-3"
|
|
T="FillerPresetViewModel"
|
|
Label="Tail Filler"
|
|
@bind-value="_selectedItem.TailFiller"
|
|
SearchFunc="@(arg => SearchFillerPresets(FillerKind.Tail, arg))"
|
|
ToStringFunc="@(c => c?.Name)"
|
|
ResetValueOnEmptyText="true"
|
|
Clearable="true"/>
|
|
<MudAutocomplete Class="mt-3"
|
|
T="FillerPresetViewModel"
|
|
Label="Fallback Filler"
|
|
@bind-value="_selectedItem.FallbackFiller"
|
|
SearchFunc="@(arg => SearchFillerPresets(FillerKind.Fallback, arg))"
|
|
ToStringFunc="@(c => c?.Name)"
|
|
ResetValueOnEmptyText="true"
|
|
Clearable="true"/>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</div>
|
|
</div>
|
|
</EditForm>
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
|
|
[Parameter]
|
|
public int Id { get; set; }
|
|
|
|
private ProgramScheduleItemsEditViewModel _schedule;
|
|
private List<MediaCollectionViewModel> _mediaCollections;
|
|
private List<MultiCollectionViewModel> _multiCollections;
|
|
private List<SmartCollectionViewModel> _smartCollections;
|
|
private List<NamedMediaItemViewModel> _televisionShows;
|
|
private List<NamedMediaItemViewModel> _televisionSeasons;
|
|
private List<NamedMediaItemViewModel> _artists;
|
|
private List<FillerPresetViewModel> _fillerPresets;
|
|
|
|
private ProgramScheduleItemEditViewModel _selectedItem;
|
|
|
|
protected override Task OnParametersSetAsync() => LoadScheduleItems();
|
|
|
|
private async Task LoadScheduleItems()
|
|
{
|
|
// TODO: fix performance
|
|
_mediaCollections = await _mediator.Send(new GetAllCollections())
|
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
|
|
_multiCollections = await _mediator.Send(new GetAllMultiCollections())
|
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
|
|
_smartCollections = await _mediator.Send(new GetAllSmartCollections())
|
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
|
|
_televisionShows = await _mediator.Send(new GetAllTelevisionShows())
|
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
|
|
_televisionSeasons = await _mediator.Send(new GetAllTelevisionSeasons())
|
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
|
|
_artists = await _mediator.Send(new GetAllArtists())
|
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
|
|
_fillerPresets = await _mediator.Send(new GetAllFillerPresets())
|
|
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
|
|
|
|
string name = string.Empty;
|
|
Option<ProgramScheduleViewModel> maybeSchedule = await _mediator.Send(new GetProgramScheduleById(Id));
|
|
maybeSchedule.IfSome(vm => name = vm.Name);
|
|
|
|
Option<IEnumerable<ProgramScheduleItemViewModel>> maybeResults = await _mediator.Send(new GetProgramScheduleItems(Id));
|
|
foreach (IEnumerable<ProgramScheduleItemViewModel> items in maybeResults)
|
|
{
|
|
_schedule = new ProgramScheduleItemsEditViewModel
|
|
{
|
|
Name = name,
|
|
Items = items.Map(ProjectToEditViewModel).ToList()
|
|
};
|
|
|
|
if (_schedule.Items.Count == 1)
|
|
{
|
|
_selectedItem = _schedule.Items.Head();
|
|
}
|
|
}
|
|
}
|
|
|
|
private ProgramScheduleItemEditViewModel ProjectToEditViewModel(ProgramScheduleItemViewModel item)
|
|
{
|
|
var result = new ProgramScheduleItemEditViewModel
|
|
{
|
|
Id = item.Id,
|
|
Index = item.Index,
|
|
StartType = item.StartType,
|
|
StartTime = item.StartTime,
|
|
PlayoutMode = item.PlayoutMode,
|
|
CollectionType = item.CollectionType,
|
|
Collection = item.Collection,
|
|
MultiCollection = item.MultiCollection,
|
|
SmartCollection = item.SmartCollection,
|
|
MediaItem = item.MediaItem,
|
|
PlaybackOrder = item.PlaybackOrder,
|
|
CustomTitle = item.CustomTitle,
|
|
GuideMode = item.GuideMode,
|
|
PreRollFiller = item.PreRollFiller,
|
|
MidRollFiller = item.MidRollFiller,
|
|
PostRollFiller = item.PostRollFiller,
|
|
TailFiller = item.TailFiller,
|
|
FallbackFiller = item.FallbackFiller
|
|
};
|
|
|
|
switch (item)
|
|
{
|
|
case ProgramScheduleItemMultipleViewModel multiple:
|
|
result.MultipleCount = multiple.Count;
|
|
break;
|
|
case ProgramScheduleItemDurationViewModel duration:
|
|
result.PlayoutDuration = duration.PlayoutDuration;
|
|
result.TailMode = duration.TailMode;
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void AddScheduleItem()
|
|
{
|
|
var item = new ProgramScheduleItemEditViewModel
|
|
{
|
|
Index = _schedule.Items.Map(i => i.Index).DefaultIfEmpty().Max() + 1,
|
|
StartType = StartType.Dynamic,
|
|
PlayoutMode = PlayoutMode.One,
|
|
PlaybackOrder = PlaybackOrder.Shuffle,
|
|
CollectionType = ProgramScheduleItemCollectionType.Collection
|
|
};
|
|
|
|
_schedule.Items.Add(item);
|
|
_selectedItem = item;
|
|
}
|
|
|
|
private void RemoveScheduleItem(ProgramScheduleItemEditViewModel item)
|
|
{
|
|
_selectedItem = null;
|
|
_schedule.Items.Remove(item);
|
|
}
|
|
|
|
private void MoveItemUp(ProgramScheduleItemEditViewModel item)
|
|
{
|
|
// swap with lower index
|
|
ProgramScheduleItemEditViewModel toSwap = _schedule.Items.OrderByDescending(x => x.Index).First(x => x.Index < item.Index);
|
|
(toSwap.Index, item.Index) = (item.Index, toSwap.Index);
|
|
}
|
|
|
|
private void MoveItemDown(ProgramScheduleItemEditViewModel item)
|
|
{
|
|
// swap with higher index
|
|
ProgramScheduleItemEditViewModel toSwap = _schedule.Items.OrderBy(x => x.Index).First(x => x.Index > item.Index);
|
|
(toSwap.Index, item.Index) = (item.Index, toSwap.Index);
|
|
}
|
|
|
|
private Task<IEnumerable<MediaCollectionViewModel>> SearchMediaCollections(string value) =>
|
|
_mediaCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<MultiCollectionViewModel>> SearchMultiCollections(string value) =>
|
|
_multiCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<SmartCollectionViewModel>> SearchSmartCollections(string value) =>
|
|
_smartCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionShows(string value) =>
|
|
_televisionShows.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionSeasons(string value) =>
|
|
_televisionSeasons.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<NamedMediaItemViewModel>> SearchArtists(string value) =>
|
|
_artists.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<FillerPresetViewModel>> SearchFillerPresets(FillerKind fillerKind, string value) =>
|
|
_fillerPresets.Filter(p => p.FillerKind == fillerKind && p.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private async Task SaveChanges()
|
|
{
|
|
var items = _schedule.Items.Map(item => new ReplaceProgramScheduleItem(
|
|
item.Index,
|
|
item.StartType,
|
|
item.StartTime,
|
|
item.PlayoutMode,
|
|
item.CollectionType,
|
|
item.Collection?.Id,
|
|
item.MultiCollection?.Id,
|
|
item.SmartCollection?.Id,
|
|
item.MediaItem?.MediaItemId,
|
|
item.PlaybackOrder,
|
|
item.MultipleCount,
|
|
item.PlayoutDuration,
|
|
item.TailMode,
|
|
item.CustomTitle,
|
|
item.GuideMode,
|
|
item.PreRollFiller?.Id,
|
|
item.MidRollFiller?.Id,
|
|
item.PostRollFiller?.Id,
|
|
item.TailFiller?.Id,
|
|
item.FallbackFiller?.Id)).ToList();
|
|
|
|
Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items)).Map(e => e.LeftToSeq());
|
|
|
|
errorMessages.HeadOrNone().Match(
|
|
error =>
|
|
{
|
|
_snackbar.Add($"Unexpected error saving schedule: {error.Value}", Severity.Error);
|
|
_logger.LogError("Unexpected error saving schedule: {Error}", error.Value);
|
|
},
|
|
() => _navigationManager.NavigateTo("/schedules"));
|
|
}
|
|
|
|
} |