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>
This commit is contained in:
@@ -10,10 +10,28 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
using HC_APTBS.ViewModels.Dialogs;
|
||||
using HC_APTBS.ViewModels.Pages;
|
||||
using HC_APTBS.Views.Dialogs;
|
||||
|
||||
namespace HC_APTBS.ViewModels
|
||||
{
|
||||
/// <summary>Identifies the top-level navigation page shown in the shell.</summary>
|
||||
public enum AppPage
|
||||
{
|
||||
/// <summary>Bench controls, flowmeter charts, encoder angles.</summary>
|
||||
Bench = 0,
|
||||
/// <summary>Pump manual control, DFI, status displays.</summary>
|
||||
Pump = 1,
|
||||
/// <summary>Test suite, live progress, results.</summary>
|
||||
Tests = 2,
|
||||
/// <summary>At-a-glance operator landing page: readings, connections, alarms, quick actions.</summary>
|
||||
Dashboard = 3,
|
||||
/// <summary>Application configuration: safety limits, PID, motor, report, K-Line, language.</summary>
|
||||
Settings = 4,
|
||||
/// <summary>Session-only history of completed test runs with detail view and PDF export.</summary>
|
||||
Results = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Root ViewModel for the application's main window.
|
||||
///
|
||||
@@ -55,6 +73,25 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>ViewModel for the non-modal unlock progress window.</summary>
|
||||
private UnlockProgressViewModel? _unlockVm;
|
||||
|
||||
/// <summary>
|
||||
/// Publicly observable accessor for the currently running (or last completed)
|
||||
/// immobilizer unlock VM. Used by the Pump page's inline unlock panel to
|
||||
/// display the same state that the floating dialog shows. Null while no
|
||||
/// unlock has been started for the current pump.
|
||||
/// </summary>
|
||||
public UnlockProgressViewModel? CurrentUnlockVm
|
||||
{
|
||||
get => _unlockVm;
|
||||
private set
|
||||
{
|
||||
if (!ReferenceEquals(_unlockVm, value))
|
||||
{
|
||||
_unlockVm = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The non-modal unlock progress window, if open.</summary>
|
||||
private UnlockProgressDialog? _unlockDlg;
|
||||
|
||||
@@ -96,6 +133,40 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>ViewModel for the second pump status display (Empf3 word).</summary>
|
||||
public StatusDisplayViewModel StatusDisplay2 { get; } = new();
|
||||
|
||||
/// <summary>ViewModel for the Dashboard's active-alarm list.</summary>
|
||||
public DashboardAlarmsViewModel DashboardAlarms { get; }
|
||||
|
||||
/// <summary>Diagnostic Trouble Code list for the Pump page §3.b sub-section.</summary>
|
||||
public DtcListViewModel DtcList { get; }
|
||||
|
||||
/// <summary>Auth gate for the Pump page §3.d Adaptation sub-section.</summary>
|
||||
public AuthGateViewModel AdaptationAuth { get; }
|
||||
|
||||
// ── Page ViewModels (thin façades over the child VMs above) ───────────────
|
||||
|
||||
/// <summary>Dashboard navigation page VM.</summary>
|
||||
public DashboardPageViewModel DashboardPage { get; private set; } = null!;
|
||||
|
||||
/// <summary>Bench navigation page VM.</summary>
|
||||
public BenchPageViewModel BenchPage { get; private set; } = null!;
|
||||
|
||||
/// <summary>Pump navigation page VM.</summary>
|
||||
public PumpPageViewModel PumpPage { get; private set; } = null!;
|
||||
|
||||
/// <summary>Tests navigation page VM.</summary>
|
||||
public TestsPageViewModel TestsPage { get; private set; } = null!;
|
||||
|
||||
/// <summary>Settings navigation page VM.</summary>
|
||||
public SettingsPageViewModel SettingsPage { get; private set; } = null!;
|
||||
|
||||
/// <summary>Results navigation page VM (session-only test-run history).</summary>
|
||||
public ResultsPageViewModel ResultsPage { get; private set; } = null!;
|
||||
|
||||
// ── Navigation state ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Currently selected top-level navigation page.</summary>
|
||||
[ObservableProperty] private AppPage _selectedPage = AppPage.Dashboard;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -130,6 +201,20 @@ namespace HC_APTBS.ViewModels
|
||||
PumpControl = new PumpControlViewModel(benchService);
|
||||
BenchControl = new BenchControlViewModel(benchService, configService);
|
||||
AngleDisplay = new AngleDisplayViewModel(configService);
|
||||
DashboardAlarms = new DashboardAlarmsViewModel(configService.Settings.Alarms);
|
||||
DtcList = new DtcListViewModel(kwpService, localizationService, logger);
|
||||
AdaptationAuth = new AuthGateViewModel(configService, localizationService);
|
||||
|
||||
// Page ViewModels are thin façades over the child VMs above; they hold a
|
||||
// reference back to this coordinator so page XAML can bind MainViewModel-owned
|
||||
// values via {Binding Root.X}.
|
||||
DashboardPage = new DashboardPageViewModel(this);
|
||||
BenchPage = new BenchPageViewModel(this, benchService, configService);
|
||||
PumpPage = new PumpPageViewModel(this, DtcList, AdaptationAuth);
|
||||
TestsPage = new TestsPageViewModel(this, configService, localizationService);
|
||||
SettingsPage = new SettingsPageViewModel(configService, localizationService);
|
||||
SettingsPage.SettingsSaved += OnSettingsSaved;
|
||||
ResultsPage = new ResultsPageViewModel(this, pdfService, configService, localizationService, logger);
|
||||
|
||||
// React to pump changes from the identification child VM.
|
||||
PumpIdentification.PumpChanged += OnPumpChanged;
|
||||
@@ -163,7 +248,12 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
CurrentPhaseName = phase;
|
||||
TestPanel.SetActivePhase(phase);
|
||||
// Clear real-time plot traces at each new phase boundary.
|
||||
FlowmeterChart.Delivery.Clear();
|
||||
FlowmeterChart.Over.Clear();
|
||||
});
|
||||
_bench.PhaseTimerTick += (section, remaining, total) => App.Current.Dispatcher.Invoke(
|
||||
() => TestPanel.ApplyPhaseTimerTick(section, remaining, total));
|
||||
_bench.VerboseMessage += msg => App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
VerboseStatus = msg;
|
||||
@@ -191,6 +281,10 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
VerboseStatus = string.Format(_loc.GetString("Error.EmergencyStop"), reason);
|
||||
});
|
||||
_bench.StatusReactionTriggered += (bit, reaction, desc) => App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
VerboseStatus = $"[STATUS] bit {bit} reaction={reaction}: {desc}";
|
||||
});
|
||||
|
||||
// Angle display: lock angle and PSG zero from test phases
|
||||
_bench.LockAngleFaseReady += () => App.Current.Dispatcher.Invoke(() =>
|
||||
@@ -281,8 +375,8 @@ namespace HC_APTBS.ViewModels
|
||||
if (pump.UnlockType == 0) return;
|
||||
|
||||
_unlockCts = new CancellationTokenSource();
|
||||
_unlockVm = new UnlockProgressViewModel(_unlock, pump.UnlockType, _unlockCts, _loc);
|
||||
_unlockDlg = new UnlockProgressDialog(_unlockVm)
|
||||
CurrentUnlockVm = new UnlockProgressViewModel(_unlock, pump.UnlockType, _unlockCts, _loc);
|
||||
_unlockDlg = new UnlockProgressDialog(_unlockVm!)
|
||||
{ Owner = Application.Current.MainWindow };
|
||||
|
||||
// Start unlock in background — ViewModel tracks via event subscriptions.
|
||||
@@ -312,7 +406,7 @@ namespace HC_APTBS.ViewModels
|
||||
if (_unlockVm != null)
|
||||
{
|
||||
_unlockVm.Dispose();
|
||||
_unlockVm = null;
|
||||
CurrentUnlockVm = null;
|
||||
}
|
||||
|
||||
if (_unlockDlg != null)
|
||||
@@ -394,6 +488,13 @@ namespace HC_APTBS.ViewModels
|
||||
/// <summary>PSG encoder position value.</summary>
|
||||
[ObservableProperty] private double _psgEncoderValue;
|
||||
|
||||
/// <summary>
|
||||
/// True when the Oil Pump relay is currently energised. Mirrored on each refresh
|
||||
/// tick from <c>_config.Bench.Relays[RelayNames.OilPump]</c> so the Tests page
|
||||
/// preconditions checklist can bind to it without walking the relay dictionary.
|
||||
/// </summary>
|
||||
[ObservableProperty] private bool _isOilPumpOn;
|
||||
|
||||
// ── Pump live readings (from pump CAN parameters) ──────────────────────────
|
||||
|
||||
/// <summary>Pump RPM reported by the ECU over CAN.</summary>
|
||||
@@ -489,6 +590,17 @@ namespace HC_APTBS.ViewModels
|
||||
|
||||
private bool CanStopTest() => IsTestRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Operator-initiated emergency stop from the Dashboard.
|
||||
/// Zeros the motor, zeros pump parameters, and cancels any running test.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void EmergencyStop()
|
||||
{
|
||||
_bench.RequestEmergencyStop("Operator pressed E-Stop on Dashboard");
|
||||
_testCts?.Cancel();
|
||||
}
|
||||
|
||||
// ── Commands: relay toggles ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>Toggles the electronic relay (pump solenoid power).</summary>
|
||||
@@ -534,7 +646,12 @@ namespace HC_APTBS.ViewModels
|
||||
{
|
||||
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
string path = _pdf.GenerateReport(
|
||||
CurrentPump, reportVm.OperatorName, reportVm.SelectedClientName, desktop);
|
||||
CurrentPump,
|
||||
reportVm.OperatorName,
|
||||
reportVm.SelectedClientName,
|
||||
desktop,
|
||||
clientInfo: reportVm.ClientInfo,
|
||||
observations: reportVm.Observations);
|
||||
_log.Info(LogId, $"Report saved: {path}");
|
||||
IsTestSaved = true;
|
||||
|
||||
@@ -552,15 +669,6 @@ namespace HC_APTBS.ViewModels
|
||||
private bool CanGenerateReport()
|
||||
=> CurrentPump != null && !IsTestRunning && CurrentPump.Tests.Count > 0;
|
||||
|
||||
// ── Commands: language toggle ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>Toggles the UI language between Spanish and English.</summary>
|
||||
[RelayCommand]
|
||||
private void ToggleLanguage()
|
||||
{
|
||||
_loc.SetLanguage(_loc.CurrentLanguage == "ESP" ? "ENG" : "ESP");
|
||||
}
|
||||
|
||||
/// <summary>Refreshes all ViewModel-cached localised strings after a language change.</summary>
|
||||
private void RefreshLocalisedStrings()
|
||||
{
|
||||
@@ -569,17 +677,13 @@ namespace HC_APTBS.ViewModels
|
||||
: _loc.GetString("Status.Disconnected");
|
||||
}
|
||||
|
||||
// ── Commands: settings ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Opens the settings dialog for editing application configuration.</summary>
|
||||
[RelayCommand]
|
||||
private void OpenSettings()
|
||||
/// <summary>
|
||||
/// Reseeds settings-dependent runtime state after the operator saves on the Settings page.
|
||||
/// Currently only the bench refresh-timer interval needs re-application.
|
||||
/// </summary>
|
||||
private void OnSettingsSaved()
|
||||
{
|
||||
var vm = new SettingsViewModel(_config, _loc);
|
||||
var dlg = new SettingsDialog(vm) { Owner = Application.Current.MainWindow };
|
||||
dlg.ShowDialog();
|
||||
|
||||
if (vm.Accepted && _refreshTimer != null)
|
||||
if (_refreshTimer != null)
|
||||
_refreshTimer.Interval = TimeSpan.FromMilliseconds(_config.Settings.RefreshBenchInterfaceMs);
|
||||
}
|
||||
|
||||
@@ -658,6 +762,15 @@ namespace HC_APTBS.ViewModels
|
||||
FlowmeterChart.AddSamples(QDelivery, QOver);
|
||||
BenchControl.RefreshFromTick();
|
||||
|
||||
// Mirror the oil pump relay state for the Tests page preconditions checklist.
|
||||
IsOilPumpOn = _config.Bench.Relays.TryGetValue(RelayNames.OilPump, out var oilRelay) && oilRelay.State;
|
||||
|
||||
// Feed page-scoped Bench VMs (pressure trace + interlock banner).
|
||||
BenchPage.RefreshFromTick();
|
||||
|
||||
// Refresh Dashboard's active-alarm list from the bench alarm bitmask.
|
||||
DashboardAlarms.Update((int)_bench.ReadBenchParameter(BenchParameterNames.Alarms));
|
||||
|
||||
if (CurrentPump != null)
|
||||
{
|
||||
PumpRpm = _bench.ReadPumpParameter(PumpParameterNames.Rpm);
|
||||
@@ -708,6 +821,7 @@ namespace HC_APTBS.ViewModels
|
||||
LastTestSuccess = !interrupted && success;
|
||||
VerboseStatus = interrupted ? _loc.GetString("Test.Stopped") : (success ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail"));
|
||||
TestPanel.IsRunning = false;
|
||||
TestPanel.ClearPhaseTimer();
|
||||
_bench.StopPumpSender();
|
||||
StartTestCommand.NotifyCanExecuteChanged();
|
||||
StopTestCommand.NotifyCanExecuteChanged();
|
||||
@@ -716,6 +830,13 @@ namespace HC_APTBS.ViewModels
|
||||
// Populate results table from all completed tests.
|
||||
if (!interrupted && CurrentPump != null)
|
||||
ResultDisplay.LoadAllResults(CurrentPump.Tests);
|
||||
|
||||
// Capture a session-only history entry (Results page §5) — covers normal
|
||||
// and interrupted completions. Snapshot is deep-cloned so later runs
|
||||
// cannot mutate this entry's data.
|
||||
if (CurrentPump != null)
|
||||
ResultsPage.CaptureRun(CurrentPump, interrupted, success);
|
||||
|
||||
_log.Info(LogId,
|
||||
$"Test finished — interrupted={interrupted}, success={success}");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user