Files
RolemasterDB/src/RolemasterDb.App/Components/Pages/Tables.razor

404 lines
14 KiB
Plaintext

@page "/tables"
@rendermode InteractiveServer
@using System
@using System.Collections.Generic
@using System.Diagnostics.CodeAnalysis
@inject IJSRuntime JSRuntime
@inject LookupService LookupService
<PageTitle>Critical Tables</PageTitle>
<section class="hero-panel">
<span class="eyebrow">Critical Tables</span>
<h1 class="page-title">Browse critical results by table</h1>
<p class="lede">Switch tables to read the full roll matrix, compare outcomes, and use the affix legend as quick play help.</p>
</section>
<section class="panel tables-page">
<div class="table-selector">
<label for="critical-table-select">Critical table</label>
<select
id="critical-table-select"
class="input-shell"
value="@selectedTableSlug"
@onchange="HandleTableChanged"
disabled="@IsTableSelectionDisabled">
@if (referenceData is null)
{
<option value="">Loading tables...</option>
}
else
{
@foreach (var table in referenceData.CriticalTables)
{
<option value="@table.Key">@table.Label</option>
}
}
</select>
</div>
@if (referenceData is null)
{
<p class="muted">Loading reference data...</p>
}
else if (!referenceData.CriticalTables.Any())
{
<p class="muted">No critical tables are available yet.</p>
}
else if (isDetailLoading)
{
<p class="muted">Loading the selected table...</p>
}
else if (!string.IsNullOrWhiteSpace(detailError))
{
<p class="error-text">@detailError</p>
}
else if (tableDetail is null)
{
<p class="muted">The selected table could not be loaded.</p>
}
else if (tableDetail is { } detail)
{
<div class="table-shell">
<header>
<h2 class="panel-title">@detail.DisplayName</h2>
</header>
<div class="table-scroll">
<table class="critical-table">
<thead>
@if (detail.Groups.Count > 0)
{
<tr>
<th class="roll-band-header" rowspan="2"></th>
@foreach (var group in detail.Groups)
{
<th colspan="@detail.Columns.Count">@group.Label</th>
}
</tr>
<tr>
@foreach (var group in detail.Groups)
{
foreach (var column in detail.Columns)
{
<th>
<span>@column.Label</span>
</th>
}
}
</tr>
}
else
{
<tr>
<th class="roll-band-header"></th>
@foreach (var column in detail.Columns)
{
<th>
<span>@column.Label</span>
</th>
}
</tr>
}
</thead>
<tbody>
@foreach (var rollBand in detail.RollBands)
{
<tr>
<th class="roll-band-header">@rollBand.Label</th>
@if (detail.Groups.Count > 0)
{
foreach (var group in detail.Groups)
{
foreach (var column in detail.Columns)
{
@if (TryGetCell(rollBand.Label, group.Key, column.Key, out var groupedCell))
{
<td
class="critical-table-cell is-editable"
tabindex="0"
title="Click to edit this cell"
@onclick="() => OpenCellEditorAsync(groupedCell.ResultId)"
@onkeydown="args => HandleCellKeyDownAsync(args, groupedCell.ResultId)">
<CompactCriticalCell
Description="@(groupedCell.Description ?? string.Empty)"
Effects="@(groupedCell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
Branches="@(groupedCell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
</td>
}
else
{
<td class="critical-table-cell">
<span class="empty-cell">—</span>
</td>
}
}
}
}
else
{
foreach (var column in detail.Columns)
{
@if (TryGetCell(rollBand.Label, null, column.Key, out var cell))
{
<td
class="critical-table-cell is-editable"
tabindex="0"
title="Click to edit this cell"
@onclick="() => OpenCellEditorAsync(cell.ResultId)"
@onkeydown="args => HandleCellKeyDownAsync(args, cell.ResultId)">
<CompactCriticalCell
Description="@(cell.Description ?? string.Empty)"
Effects="@(cell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
Branches="@(cell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
</td>
}
else
{
<td class="critical-table-cell">
<span class="empty-cell">—</span>
</td>
}
}
}
</tr>
}
</tbody>
</table>
</div>
@{
var legendEntries = detail.Legend ?? Array.Empty<CriticalTableLegendEntry>();
}
@if (legendEntries.Count > 0)
{
<div class="critical-legend">
<h4>Affix legend</h4>
<div class="legend-grid">
@foreach (var entry in legendEntries)
{
<div class="legend-item" title="@entry.Tooltip">
<span class="legend-symbol">@entry.Symbol</span>
<div>
<strong>@entry.Label</strong>
<span class="muted">@entry.Description</span>
</div>
</div>
}
</div>
</div>
}
</div>
}
</section>
@if (isEditorOpen)
{
<CriticalCellEditorDialog
Model="editorModel"
IsLoading="isEditorLoading"
IsSaving="isEditorSaving"
LoadErrorMessage="@editorLoadError"
SaveErrorMessage="@editorSaveError"
OnClose="CloseCellEditorAsync"
OnSave="SaveCellEditorAsync" />
}
@code {
private LookupReferenceData? referenceData;
private CriticalTableDetail? tableDetail;
private string selectedTableSlug = string.Empty;
private bool isDetailLoading;
private bool isReferenceDataLoading = true;
private string? detailError;
private Dictionary<(string RollBand, string? GroupKey, string ColumnKey), CriticalTableCellDetail>? cellIndex;
private int tableLayoutVersion;
private int appliedLayoutVersion = -1;
private bool IsTableSelectionDisabled => isReferenceDataLoading || (referenceData?.CriticalTables.Count ?? 0) == 0;
private bool isEditorOpen;
private bool isEditorLoading;
private bool isEditorSaving;
private string? editorLoadError;
private string? editorSaveError;
private int? editingResultId;
private CriticalCellEditorModel? editorModel;
protected override async Task OnInitializedAsync()
{
referenceData = await LookupService.GetReferenceDataAsync();
isReferenceDataLoading = false;
selectedTableSlug = referenceData?.CriticalTables.FirstOrDefault()?.Key ?? string.Empty;
await LoadTableDetailAsync();
}
private async Task HandleTableChanged(ChangeEventArgs args)
{
selectedTableSlug = args.Value?.ToString() ?? string.Empty;
await LoadTableDetailAsync();
}
private async Task LoadTableDetailAsync()
{
if (string.IsNullOrWhiteSpace(selectedTableSlug))
{
tableDetail = null;
cellIndex = null;
tableLayoutVersion++;
return;
}
isDetailLoading = true;
detailError = null;
tableDetail = null;
cellIndex = null;
try
{
tableDetail = await LookupService.GetCriticalTableAsync(selectedTableSlug);
if (tableDetail is null)
{
detailError = "The selected table could not be loaded.";
}
}
catch (Exception exception)
{
detailError = exception.Message;
}
finally
{
isDetailLoading = false;
BuildCellIndex();
tableLayoutVersion++;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (tableDetail is null || appliedLayoutVersion == tableLayoutVersion)
{
return;
}
await JSRuntime.InvokeVoidAsync("rolemasterTables.alignCriticalCells");
appliedLayoutVersion = tableLayoutVersion;
}
private void BuildCellIndex()
{
if (tableDetail?.Cells is null)
{
cellIndex = null;
return;
}
cellIndex = new Dictionary<(string, string?, string), CriticalTableCellDetail>();
foreach (var cell in tableDetail.Cells)
{
cellIndex[(cell.RollBand, cell.GroupKey, cell.ColumnKey)] = cell;
}
}
private bool TryGetCell(string rollBand, string? groupKey, string columnKey, [NotNullWhen(true)] out CriticalTableCellDetail? cell)
{
if (cellIndex is null)
{
cell = null;
return false;
}
return cellIndex.TryGetValue((rollBand, groupKey, columnKey), out cell);
}
private async Task OpenCellEditorAsync(int resultId)
{
if (string.IsNullOrWhiteSpace(selectedTableSlug))
{
return;
}
editorLoadError = null;
editorSaveError = null;
editorModel = null;
editingResultId = resultId;
isEditorSaving = false;
isEditorLoading = true;
isEditorOpen = true;
try
{
var response = await LookupService.GetCriticalCellEditorAsync(selectedTableSlug, resultId);
if (response is null)
{
editorLoadError = "The selected cell could not be loaded for editing.";
editorModel = null;
return;
}
editorModel = CriticalCellEditorModel.FromResponse(response);
}
catch (Exception exception)
{
editorLoadError = exception.Message;
editorModel = null;
}
finally
{
isEditorLoading = false;
}
}
private async Task CloseCellEditorAsync()
{
isEditorOpen = false;
isEditorLoading = false;
isEditorSaving = false;
editorLoadError = null;
editorSaveError = null;
editingResultId = null;
editorModel = null;
await InvokeAsync(StateHasChanged);
}
private async Task SaveCellEditorAsync()
{
if (editorModel is null || string.IsNullOrWhiteSpace(selectedTableSlug) || editingResultId is null)
{
return;
}
isEditorSaving = true;
editorSaveError = null;
try
{
var response = await LookupService.UpdateCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest());
if (response is null)
{
editorSaveError = "The selected cell could not be saved.";
return;
}
await LoadTableDetailAsync();
await CloseCellEditorAsync();
}
catch (Exception exception)
{
editorSaveError = exception.Message;
}
finally
{
isEditorSaving = false;
}
}
private async Task HandleCellKeyDownAsync(KeyboardEventArgs args, int resultId)
{
if (args.Key is "Enter" or " ")
{
await OpenCellEditorAsync(resultId);
}
}
}