Files
HC_APTBS/ViewModels/TestPreconditionsViewModel.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

281 lines
12 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Preconditions checklist for the Tests page "Preconditions" wizard step.
///
/// <para>Aggregates the seven safety/readiness checks specified in
/// <c>docs/ui-structure.md</c> §4b. Items auto-refresh whenever the underlying
/// <see cref="MainViewModel"/> properties change; <see cref="StartTestCommand"/>
/// stays disabled until every required check passes.</para>
///
/// <para>Subscriptions are established in <see cref="Activate"/> and released in
/// <see cref="Deactivate"/>; the parent view-model calls these as the wizard state
/// transitions into/out of Preconditions so we do not do work during Plan/Running/Done.</para>
/// </summary>
public sealed partial class TestPreconditionsViewModel : ObservableObject
{
// ── Stable item identifiers ───────────────────────────────────────────────
private const string IdPump = "pump";
private const string IdCan = "can";
private const string IdKLine = "kline";
private const string IdRpmZero = "rpmZero";
private const string IdOilPump = "oilPump";
private const string IdNoAlarms = "noAlarms";
private const string IdAuth = "auth";
// ── Resource keys ─────────────────────────────────────────────────────────
private const string KeyLabelPump = "Test.Precheck.PumpSelected";
private const string KeyLabelCan = "Test.Precheck.CanLive";
private const string KeyLabelKLine = "Test.Precheck.KLineOpen";
private const string KeyLabelRpmZero = "Test.Precheck.RpmZero";
private const string KeyLabelOilPump = "Test.Precheck.OilPumpOn";
private const string KeyLabelNoAlarms = "Test.Precheck.NoCriticalAlarms";
private const string KeyLabelAuth = "Test.Precheck.UserAuth";
private const string KeyRemPump = "Test.Precheck.Remediation.SelectPump";
private const string KeyRemCan = "Test.Precheck.Remediation.CheckCan";
private const string KeyRemKLine = "Test.Precheck.Remediation.OpenKLine";
private const string KeyRemRpmZero = "Test.Precheck.Remediation.StopBench";
private const string KeyRemOilPump = "Test.Precheck.Remediation.StartOilPump";
private const string KeyRemNoAlarms = "Test.Precheck.Remediation.ClearAlarms";
private const string KeyRemAuth = "Test.Precheck.Remediation.Authenticate";
private readonly MainViewModel _root;
private readonly ILocalizationService _loc;
private readonly TestPanelViewModel _testPanel;
private bool _subscribed;
/// <summary>Rows rendered by the checklist view, in display order.</summary>
public ObservableCollection<PreconditionItemViewModel> Items { get; } = new();
/// <summary>Gate used to authenticate the operator when a required test has <see cref="TestDefinition.RequiresAuth"/>.</summary>
public AuthGateViewModel TestAuth { get; }
/// <summary>True when the currently-enabled tests include at least one requiring authentication.</summary>
[ObservableProperty] private bool _isAuthRequired;
/// <summary>True when every required check passes — gates <see cref="StartTestCommand"/>.</summary>
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartTestCommand))]
private bool _allPassed;
/// <param name="root">Root VM — source of all live bench/ECU state.</param>
/// <param name="loc">Localisation service for label refresh.</param>
/// <param name="testPanel">Panel VM — used to discover which tests are enabled and whether any require auth.</param>
/// <param name="testAuth">Auth gate scoped to the Tests page.</param>
public TestPreconditionsViewModel(
MainViewModel root,
ILocalizationService loc,
TestPanelViewModel testPanel,
AuthGateViewModel testAuth)
{
_root = root;
_loc = loc;
_testPanel = testPanel;
TestAuth = testAuth;
BuildItems();
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
/// <summary>
/// Called by the parent when the wizard enters the Preconditions state.
/// Subscribes to all live-state sources and evaluates once.
/// </summary>
public void Activate()
{
if (_subscribed) return;
_root.PropertyChanged += OnRootPropertyChanged;
_root.DashboardAlarms.PropertyChanged += OnAlarmsPropertyChanged;
TestAuth.PropertyChanged += OnAuthPropertyChanged;
_loc.LanguageChanged += OnLanguageChanged;
_subscribed = true;
RefreshAuthRequired();
RebuildAuthItemVisibility();
Reevaluate();
}
/// <summary>Called by the parent when the wizard leaves the Preconditions state.</summary>
public void Deactivate()
{
if (!_subscribed) return;
_root.PropertyChanged -= OnRootPropertyChanged;
_root.DashboardAlarms.PropertyChanged -= OnAlarmsPropertyChanged;
TestAuth.PropertyChanged -= OnAuthPropertyChanged;
_loc.LanguageChanged -= OnLanguageChanged;
_subscribed = false;
}
// ── Build ─────────────────────────────────────────────────────────────────
private void BuildItems()
{
Items.Clear();
Items.Add(new PreconditionItemViewModel(IdPump, _root, AppPage.Pump));
Items.Add(new PreconditionItemViewModel(IdCan, _root, AppPage.Dashboard));
Items.Add(new PreconditionItemViewModel(IdKLine, _root, AppPage.Pump));
Items.Add(new PreconditionItemViewModel(IdRpmZero, _root, AppPage.Bench));
Items.Add(new PreconditionItemViewModel(IdOilPump, _root, AppPage.Bench));
Items.Add(new PreconditionItemViewModel(IdNoAlarms, _root, AppPage.Dashboard));
// Auth item added on-demand (see RebuildAuthItemVisibility).
RefreshLabels();
}
private void RebuildAuthItemVisibility()
{
var authItem = Items.FirstOrDefault(i => i.Id == IdAuth);
if (IsAuthRequired && authItem == null)
{
Items.Add(new PreconditionItemViewModel(IdAuth, _root, remediationTargetPage: null));
RefreshLabels();
}
else if (!IsAuthRequired && authItem != null)
{
Items.Remove(authItem);
}
}
// ── Evaluation ────────────────────────────────────────────────────────────
/// <summary>Recomputes every item's satisfied state and <see cref="AllPassed"/>.</summary>
public void Reevaluate()
{
foreach (var item in Items)
item.IsSatisfied = EvaluateItem(item.Id);
AllPassed = Items.All(i => !i.IsRequired || i.IsSatisfied);
}
private bool EvaluateItem(string id) => id switch
{
IdPump => _root.CurrentPump != null,
IdCan => _root.IsCanConnected,
IdKLine => _root.KLineState == KLineConnectionState.Connected,
IdRpmZero => _root.BenchRpm == 0,
IdOilPump => _root.IsOilPumpOn,
IdNoAlarms => !_root.DashboardAlarms.HasCritical,
IdAuth => TestAuth.IsAuthenticated,
_ => true,
};
private void RefreshAuthRequired()
{
IsAuthRequired = _testPanel.Tests
.Any(s => (s.Source?.RequiresAuth ?? false) && s.Phases.Any(p => p.IsEnabled));
}
// ── Labels ────────────────────────────────────────────────────────────────
private void RefreshLabels()
{
foreach (var item in Items)
{
item.Label = _loc.GetString(LabelKeyFor(item.Id));
item.RemediationText = _loc.GetString(RemediationKeyFor(item.Id));
}
}
private static string LabelKeyFor(string id) => id switch
{
IdPump => KeyLabelPump,
IdCan => KeyLabelCan,
IdKLine => KeyLabelKLine,
IdRpmZero => KeyLabelRpmZero,
IdOilPump => KeyLabelOilPump,
IdNoAlarms => KeyLabelNoAlarms,
IdAuth => KeyLabelAuth,
_ => id,
};
private static string RemediationKeyFor(string id) => id switch
{
IdPump => KeyRemPump,
IdCan => KeyRemCan,
IdKLine => KeyRemKLine,
IdRpmZero => KeyRemRpmZero,
IdOilPump => KeyRemOilPump,
IdNoAlarms => KeyRemNoAlarms,
IdAuth => KeyRemAuth,
_ => string.Empty,
};
// ── Event handlers ────────────────────────────────────────────────────────
private void OnRootPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(MainViewModel.CurrentPump):
case nameof(MainViewModel.IsCanConnected):
case nameof(MainViewModel.KLineState):
case nameof(MainViewModel.BenchRpm):
case nameof(MainViewModel.IsOilPumpOn):
Reevaluate();
break;
}
}
private void OnAlarmsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DashboardAlarmsViewModel.HasCritical))
Reevaluate();
}
private void OnAuthPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AuthGateViewModel.IsAuthenticated))
Reevaluate();
}
private void OnLanguageChanged() => RefreshLabels();
/// <summary>
/// Called by the parent VM whenever the test-panel enabled-phase selection changes,
/// so the auth item can be shown/hidden based on enabled tests' <see cref="TestDefinition.RequiresAuth"/>.
/// </summary>
public void OnEnabledPhasesChanged()
{
RefreshAuthRequired();
RebuildAuthItemVisibility();
Reevaluate();
}
partial void OnIsAuthRequiredChanged(bool value)
{
RebuildAuthItemVisibility();
Reevaluate();
}
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>Delegates to <see cref="MainViewModel.StartTestCommand"/> when <see cref="AllPassed"/> is true.</summary>
[RelayCommand(CanExecute = nameof(CanStart))]
private void StartTest()
{
if (_root.StartTestCommand.CanExecute(null))
_root.StartTestCommand.Execute(null);
}
private bool CanStart() => AllPassed;
}
}