404 lines
14 KiB
Plaintext
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);
|
|
}
|
|
}
|
|
}
|