Files
HC_APTBS/ViewModels/Pages/TestsPageViewModel.cs
LucianoDev 0280a2fad1 feat: page-based navigation shell + Tests page wizard
Replace the monolithic MainWindow with a SelectedPage-driven shell
(Dashboard / Pump / Bench / Tests / Results / Settings). The Tests
page gets the Plan -> Preconditions -> Running -> Done wizard from
ui-structure.md \u00a74, backed by a 7-item precondition gate and
shared sub-views (PhaseCardView / TestSectionView / GraphicIndicatorView)
extracted from the now-deleted monolithic TestPanelView.

New VMs / views:
- Tests wizard: TestPreconditions, PhaseCard, GraphicIndicator,
  TestSection, TestPlan, TestRunning, TestDone
- Dashboard panels: DashboardConnection, DashboardReadings,
  DashboardAlarms, InterlockBanner, ResultHistory
- Pump / bench panels: PumpIdentificationPanel, PumpLiveData,
  UnlockPanel, BenchDriveControl, BenchReadings, RelayBank,
  TemperatureControl, DtcList, AuthGate
- Dialogs: generic ConfirmDialog, UserManageDialog, UserPromptDialog

Supporting changes:
- IsOilPumpOn exposed on MainViewModel for precondition evaluation
- RequiresAuth added to TestDefinition (XML round-trip)
- BipStatusDefinition + CompletedTestRun models
- ~35 new Test.* localization keys (en + es)
- Settings moved from modal dialog to full page
- Pause / Retry / Skip stubs in TestRunningView; full spec in
  docs/gap-test-running-controls.md for follow-up implementation
- docs/ui-structure.md captures the wizard design

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 13:11:34 +02:00

234 lines
10 KiB
C#

using System.ComponentModel;
using System.Linq;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models;
using HC_APTBS.Services;
using HC_APTBS.ViewModels.Dialogs;
using HC_APTBS.Views.Dialogs;
namespace HC_APTBS.ViewModels.Pages
{
/// <summary>Wrapper VM exposing <see cref="TestPanel"/> when the wizard is in the Plan step.</summary>
public sealed class PlanStateViewModel
{
/// <summary>Shared test panel (enable/disable phases).</summary>
public TestPanelViewModel TestPanel { get; }
/// <summary>Creates a new Plan-state wrapper around the shared test panel.</summary>
public PlanStateViewModel(TestPanelViewModel testPanel) => TestPanel = testPanel;
}
/// <summary>Wrapper VM exposing <see cref="TestPanel"/> when the wizard is in the Running step.</summary>
public sealed class RunningStateViewModel
{
/// <summary>Shared test panel (live phase updates).</summary>
public TestPanelViewModel TestPanel { get; }
/// <summary>Creates a new Running-state wrapper around the shared test panel.</summary>
public RunningStateViewModel(TestPanelViewModel testPanel) => TestPanel = testPanel;
}
/// <summary>
/// Orchestrator view-model for the Tests navigation page.
///
/// <para>Drives the <c>Plan → Preconditions → Running → Done</c> wizard defined in
/// <c>docs/ui-structure.md §4</c>. Exposes <see cref="CurrentStateVm"/>, which the
/// view's <c>ContentControl</c> routes through typed DataTemplates to the four
/// step views. Commands (<see cref="NextCommand"/>, <see cref="BackCommand"/>,
/// <see cref="AbortCommand"/>, <see cref="RunAgainCommand"/>,
/// <see cref="ViewFullResultsCommand"/>) form the wizard's state-machine edges.</para>
///
/// <para>Observes <see cref="MainViewModel.IsTestRunning"/> to perform the
/// Preconditions→Running (on true) and Running→Done (on false) transitions
/// automatically, so the page stays in sync regardless of which control fired
/// the underlying start/stop command.</para>
/// </summary>
public sealed partial class TestsPageViewModel : ObservableObject
{
private readonly IConfigurationService _config;
private readonly ILocalizationService _loc;
/// <summary>Root ViewModel — owns services, live readings, and global commands.</summary>
public MainViewModel Root { get; }
/// <summary>Test panel: sections, phase cards, live indicators.</summary>
public TestPanelViewModel TestPanel => Root.TestPanel;
/// <summary>Measurement results table (per-phase pass/fail).</summary>
public ResultDisplayViewModel ResultDisplay => Root.ResultDisplay;
/// <summary>Preconditions checklist — lazily instantiated on first entry into the step.</summary>
public TestPreconditionsViewModel Preconditions { get; }
/// <summary>Auth gate scoped to the Tests page (used by preconditions for auth-required tests).</summary>
public AuthGateViewModel TestAuth { get; }
private readonly PlanStateViewModel _planVm;
private readonly RunningStateViewModel _runningVm;
/// <summary>
/// Creates the Tests page orchestrator.
/// </summary>
/// <param name="root">Root coordinator.</param>
/// <param name="config">Configuration service (passed to the scoped auth gate).</param>
/// <param name="loc">Localisation service.</param>
public TestsPageViewModel(MainViewModel root, IConfigurationService config, ILocalizationService loc)
{
Root = root;
_config = config;
_loc = loc;
TestAuth = new AuthGateViewModel(config, loc);
Preconditions = new TestPreconditionsViewModel(root, loc, Root.TestPanel, TestAuth);
_planVm = new PlanStateViewModel(Root.TestPanel);
_runningVm = new RunningStateViewModel(Root.TestPanel);
CurrentStateVm = _planVm;
Root.PropertyChanged += OnRootPropertyChanged;
}
// ── State ─────────────────────────────────────────────────────────────────
/// <summary>Current wizard step.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
[NotifyCanExecuteChangedFor(nameof(AbortCommand))]
[NotifyCanExecuteChangedFor(nameof(RunAgainCommand))]
[NotifyCanExecuteChangedFor(nameof(ViewFullResultsCommand))]
private TestFlowState _currentState = TestFlowState.Plan;
/// <summary>
/// View-model currently rendered by the step <c>ContentControl</c>. Swaps to
/// <see cref="PlanStateViewModel"/>, <see cref="TestPreconditionsViewModel"/>,
/// <see cref="RunningStateViewModel"/>, or <c>this</c> (for the Done step).
/// </summary>
[ObservableProperty] private object _currentStateVm;
/// <summary>Convenience flag for view styling — true while a test is actively running.</summary>
public bool IsRunningStep => CurrentState == TestFlowState.Running;
/// <summary>Convenience flag for view styling — true when the page is on the Done step.</summary>
public bool IsDoneStep => CurrentState == TestFlowState.Done;
partial void OnCurrentStateChanged(TestFlowState oldValue, TestFlowState newValue)
{
if (oldValue == TestFlowState.Preconditions && newValue != TestFlowState.Preconditions)
Preconditions.Deactivate();
switch (newValue)
{
case TestFlowState.Plan:
CurrentStateVm = _planVm;
break;
case TestFlowState.Preconditions:
Preconditions.Activate();
Preconditions.OnEnabledPhasesChanged();
CurrentStateVm = Preconditions;
break;
case TestFlowState.Running:
CurrentStateVm = _runningVm;
break;
case TestFlowState.Done:
CurrentStateVm = this;
break;
}
OnPropertyChanged(nameof(IsRunningStep));
OnPropertyChanged(nameof(IsDoneStep));
}
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>
/// Advances Plan → Preconditions. No-op if no phases are enabled (defensive — the
/// Preconditions step would fail its auth-detection anyway so there's nothing to
/// start). The phase-enable state is not observed live, so the button itself is
/// only guarded by <see cref="CurrentState"/>.
/// </summary>
[RelayCommand(CanExecute = nameof(CanNext))]
private void Next()
{
if (CurrentState != TestFlowState.Plan) return;
if (!Root.TestPanel.Tests.Any(s => s.Phases.Any(p => p.IsEnabled))) return;
CurrentState = TestFlowState.Preconditions;
}
private bool CanNext() => CurrentState == TestFlowState.Plan;
/// <summary>Goes back Preconditions → Plan. Disabled during Running / Done.</summary>
[RelayCommand(CanExecute = nameof(CanBack))]
private void Back()
{
if (CurrentState == TestFlowState.Preconditions)
CurrentState = TestFlowState.Plan;
}
private bool CanBack() => CurrentState == TestFlowState.Preconditions;
/// <summary>
/// Opens a confirmation dialog and, if accepted, delegates to <see cref="MainViewModel.StopTestCommand"/>.
/// </summary>
[RelayCommand(CanExecute = nameof(CanAbort))]
private void Abort()
{
var vm = new ConfirmDialogViewModel
{
Title = _loc.GetString("Test.Abort.Title"),
Message = _loc.GetString("Test.Abort.Message"),
ConfirmText = _loc.GetString("Test.Abort.Confirm"),
CancelText = _loc.GetString("Test.Abort.Cancel"),
};
var dlg = new ConfirmDialog(vm) { Owner = Application.Current.MainWindow };
dlg.ShowDialog();
if (!vm.Accepted) return;
if (Root.StopTestCommand.CanExecute(null))
Root.StopTestCommand.Execute(null);
}
private bool CanAbort() => CurrentState == TestFlowState.Running;
/// <summary>Resets the page for a fresh run without reloading the pump.</summary>
[RelayCommand(CanExecute = nameof(CanRunAgain))]
private void RunAgain()
{
Root.TestPanel.ResetResults();
Root.ResultDisplay.Clear();
CurrentState = TestFlowState.Plan;
}
private bool CanRunAgain() => CurrentState == TestFlowState.Done;
/// <summary>Jumps to the Results navigation page.</summary>
[RelayCommand(CanExecute = nameof(CanViewFullResults))]
private void ViewFullResults()
{
Root.SelectedPage = AppPage.Results;
}
private bool CanViewFullResults() => CurrentState == TestFlowState.Done;
// ── IsTestRunning → wizard state sync ─────────────────────────────────────
private void OnRootPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(MainViewModel.IsTestRunning)) return;
if (Root.IsTestRunning)
{
if (CurrentState == TestFlowState.Preconditions)
CurrentState = TestFlowState.Running;
}
else
{
if (CurrentState == TestFlowState.Running)
CurrentState = TestFlowState.Done;
}
}
}
}