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:
66
ViewModels/Pages/BenchPageViewModel.cs
Normal file
66
ViewModels/Pages/BenchPageViewModel.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using HC_APTBS.Services;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the Bench navigation page.
|
||||
///
|
||||
/// <para>Groups the bench-related child ViewModels owned by
|
||||
/// <see cref="MainViewModel"/> together with page-specific façades
|
||||
/// (temperature control, relay bank, pressure trace, interlock banner)
|
||||
/// that only the Bench page consumes.</para>
|
||||
/// </summary>
|
||||
public sealed class BenchPageViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>Root ViewModel — owns services, live readings, and global commands.</summary>
|
||||
public MainViewModel Root { get; }
|
||||
|
||||
/// <summary>Manual bench controls (direction, RPM start/stop, oil pump, counter).</summary>
|
||||
public BenchControlViewModel BenchControl => Root.BenchControl;
|
||||
|
||||
/// <summary>Real-time flowmeter charts (Q-Delivery, Q-Over).</summary>
|
||||
public FlowmeterChartViewModel FlowmeterChart => Root.FlowmeterChart;
|
||||
|
||||
/// <summary>Encoder angle monitoring (PSG, INJ, Manual, Lock Angle).</summary>
|
||||
public AngleDisplayViewModel AngleDisplay => Root.AngleDisplay;
|
||||
|
||||
/// <summary>Temperature PID setpoint + heater / cooler relay toggles.</summary>
|
||||
public TemperatureControlViewModel TempControl { get; }
|
||||
|
||||
/// <summary>Auxiliary relay toggle bank (Electronic, Flasher, Pulse4Signal).</summary>
|
||||
public RelayBankViewModel RelayBank { get; }
|
||||
|
||||
/// <summary>Real-time pressure traces (P1, P2).</summary>
|
||||
public PressureTraceChartViewModel PressureTrace { get; } = new();
|
||||
|
||||
/// <summary>Soft safety interlock banner state (oil pump / RPM limit).</summary>
|
||||
public InterlockBannerViewModel Interlock { get; }
|
||||
|
||||
/// <param name="root">Root view-model providing live bench readings and child VMs.</param>
|
||||
/// <param name="benchService">Bench service for setpoint and relay control.</param>
|
||||
/// <param name="configService">Configuration service for safety limits and defaults.</param>
|
||||
public BenchPageViewModel(
|
||||
MainViewModel root,
|
||||
IBenchService benchService,
|
||||
IConfigurationService configService)
|
||||
{
|
||||
Root = root;
|
||||
TempControl = new TemperatureControlViewModel(benchService, configService);
|
||||
RelayBank = new RelayBankViewModel(benchService);
|
||||
Interlock = new InterlockBannerViewModel(configService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from <see cref="MainViewModel.OnRefreshTick"/> to feed the
|
||||
/// page-scoped VMs (pressure trace, interlock banner) from the latest
|
||||
/// bench readings. Keeping this here avoids adding page-specific logic
|
||||
/// to the root ViewModel.
|
||||
/// </summary>
|
||||
public void RefreshFromTick()
|
||||
{
|
||||
PressureTrace.AddSamples(Root.Pressure, Root.Pressure2);
|
||||
Interlock.Update(Root.BenchRpm, BenchControl.IsOilPumpOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
ViewModels/Pages/DashboardPageViewModel.cs
Normal file
25
ViewModels/Pages/DashboardPageViewModel.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the Dashboard navigation page.
|
||||
///
|
||||
/// <para>Thin façade — holds a <see cref="Root"/> reference so the Dashboard XAML
|
||||
/// can bind to MainViewModel-owned live readings, connection state, test summary,
|
||||
/// alarms, and commands via <c>{Binding Root.X}</c>.</para>
|
||||
/// </summary>
|
||||
public sealed class DashboardPageViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>Root ViewModel — owns services, live readings, and global commands.</summary>
|
||||
public MainViewModel Root { get; }
|
||||
|
||||
/// <summary>Active alarm aggregator bound to the Dashboard alarm list.</summary>
|
||||
public DashboardAlarmsViewModel Alarms => Root.DashboardAlarms;
|
||||
|
||||
public DashboardPageViewModel(MainViewModel root)
|
||||
{
|
||||
Root = root;
|
||||
}
|
||||
}
|
||||
}
|
||||
132
ViewModels/Pages/PumpPageViewModel.cs
Normal file
132
ViewModels/Pages/PumpPageViewModel.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.ViewModels.Dialogs;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Pages
|
||||
{
|
||||
/// <summary>Identifies the sub-section shown inside the Pump navigation page.</summary>
|
||||
public enum PumpSubPage
|
||||
{
|
||||
/// <summary>§3.a — Pump selection and K-Line ECU read.</summary>
|
||||
Identification = 0,
|
||||
/// <summary>§3.b — Diagnostic Trouble Codes.</summary>
|
||||
Dtcs = 1,
|
||||
/// <summary>§3.c — Live pump CAN readings and status words.</summary>
|
||||
LiveData = 2,
|
||||
/// <summary>§3.d — DFI calibration and ME/FBKW/PreIn manual control (auth-gated).</summary>
|
||||
Adaptation = 3,
|
||||
/// <summary>§3.e — Ford VP44 immobilizer unlock (visible only when required).</summary>
|
||||
Unlock = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the Pump navigation page.
|
||||
///
|
||||
/// <para>Thin façade that groups the pump-related child ViewModels owned by
|
||||
/// <see cref="MainViewModel"/> and adds sub-page navigation, banner flags,
|
||||
/// and the Adaptation auth gate. Holds a <see cref="Root"/> reference so
|
||||
/// page XAML can bind to MainViewModel-owned properties (PumpRpm, PumpTemp,
|
||||
/// KLineState, …) via <c>{Binding Root.X}</c>.</para>
|
||||
/// </summary>
|
||||
public sealed partial class PumpPageViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>Root ViewModel — owns services, live readings, and global commands.</summary>
|
||||
public MainViewModel Root { get; }
|
||||
|
||||
// ── Child VM façades ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Pump selector and K-Line read (§3.a).</summary>
|
||||
public PumpIdentificationViewModel Identification => Root.PumpIdentification;
|
||||
|
||||
/// <summary>Diagnostic Trouble Code list (§3.b).</summary>
|
||||
public DtcListViewModel DtcList { get; }
|
||||
|
||||
/// <summary>Adaptation sub-section auth gate (§3.d).</summary>
|
||||
public AuthGateViewModel AdaptationAuth { get; }
|
||||
|
||||
/// <summary>DFI management (§3.d).</summary>
|
||||
public DfiManageViewModel DfiViewModel => Root.DfiViewModel;
|
||||
|
||||
/// <summary>Manual pump control sliders (§3.d).</summary>
|
||||
public PumpControlViewModel PumpControl => Root.PumpControl;
|
||||
|
||||
/// <summary>First pump status display — Status word (§3.c).</summary>
|
||||
public StatusDisplayViewModel StatusDisplay1 => Root.StatusDisplay1;
|
||||
|
||||
/// <summary>Second pump status display — Empf3 word (§3.c).</summary>
|
||||
public StatusDisplayViewModel StatusDisplay2 => Root.StatusDisplay2;
|
||||
|
||||
/// <summary>Current immobilizer unlock VM (§3.e). Null when no unlock is in progress for this pump.</summary>
|
||||
public UnlockProgressViewModel? UnlockVm => Root.CurrentUnlockVm;
|
||||
|
||||
// ── Navigation state ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Currently selected Pump sub-section.</summary>
|
||||
[ObservableProperty] private PumpSubPage _selectedSubPage = PumpSubPage.Identification;
|
||||
|
||||
// ── Banner flags (derived from Root state) ────────────────────────────────
|
||||
|
||||
/// <summary>True when a pump has been loaded from the database.</summary>
|
||||
[ObservableProperty] private bool _isPumpSelected;
|
||||
|
||||
/// <summary>True when the K-Line session is currently open.</summary>
|
||||
[ObservableProperty] private bool _isKLineSessionOpen;
|
||||
|
||||
/// <summary>True when the K-Line session is in the failed state.</summary>
|
||||
[ObservableProperty] private bool _isKLineSessionFailed;
|
||||
|
||||
/// <summary>True for pumps that require a Ford immobilizer unlock (Type 1 or 2).</summary>
|
||||
[ObservableProperty] private bool _isUnlockApplicable;
|
||||
|
||||
/// <summary>Constructs the page VM and subscribes to relevant Root state changes.</summary>
|
||||
public PumpPageViewModel(
|
||||
MainViewModel root,
|
||||
DtcListViewModel dtcList,
|
||||
AuthGateViewModel adaptationAuth)
|
||||
{
|
||||
Root = root;
|
||||
DtcList = dtcList;
|
||||
AdaptationAuth = adaptationAuth;
|
||||
|
||||
// Initialise derived flags from the current Root state.
|
||||
RefreshDerivedFlags();
|
||||
|
||||
// Keep the derived flags in sync with Root changes.
|
||||
Root.PropertyChanged += OnRootPropertyChanged;
|
||||
Root.PumpIdentification.PumpChanged += _ => RefreshDerivedFlags();
|
||||
}
|
||||
|
||||
private void OnRootPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(MainViewModel.KLineState):
|
||||
IsKLineSessionOpen = Root.KLineState == KLineConnectionState.Connected;
|
||||
IsKLineSessionFailed = Root.KLineState == KLineConnectionState.Failed;
|
||||
break;
|
||||
|
||||
case nameof(MainViewModel.CurrentUnlockVm):
|
||||
OnPropertyChanged(nameof(UnlockVm));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshDerivedFlags()
|
||||
{
|
||||
IsPumpSelected = Root.CurrentPump != null;
|
||||
IsKLineSessionOpen = Root.KLineState == KLineConnectionState.Connected;
|
||||
IsKLineSessionFailed = Root.KLineState == KLineConnectionState.Failed;
|
||||
IsUnlockApplicable = Root.CurrentPump != null && Root.CurrentPump.UnlockType != 0;
|
||||
OnPropertyChanged(nameof(UnlockVm));
|
||||
|
||||
// When the pump changes, re-lock the adaptation gate — a new operator
|
||||
// may be handling a different pump.
|
||||
if (AdaptationAuth.IsAuthenticated)
|
||||
AdaptationAuth.LockCommand.Execute(null);
|
||||
|
||||
// Drop any stale DTCs from the previous pump.
|
||||
DtcList.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
273
ViewModels/Pages/ResultsPageViewModel.cs
Normal file
273
ViewModels/Pages/ResultsPageViewModel.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Xml.Linq;
|
||||
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>
|
||||
/// ViewModel for the Results navigation page (§5 in <c>docs/ui-structure.md</c>).
|
||||
///
|
||||
/// <para>Owns a session-only history of completed test runs, drives the embedded
|
||||
/// <see cref="ResultDisplayViewModel"/> detail pane, hosts the operator
|
||||
/// observations/notes field, and runs the PDF export flow against the selected
|
||||
/// snapshot.</para>
|
||||
/// </summary>
|
||||
public sealed partial class ResultsPageViewModel : ObservableObject
|
||||
{
|
||||
// ── Services ──────────────────────────────────────────────────────────────
|
||||
|
||||
private readonly IPdfService _pdf;
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
private readonly IAppLogger _log;
|
||||
private const string LogId = "ResultsPage";
|
||||
|
||||
/// <summary>Remembers the last authenticated user to pre-fill the next auth dialog.</summary>
|
||||
private string _lastAuthenticatedUser = string.Empty;
|
||||
|
||||
// ── Root reference ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Root ViewModel — lets page XAML bind app-global state when needed.</summary>
|
||||
public MainViewModel Root { get; }
|
||||
|
||||
// ── Child VMs ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Detail-pane ViewModel driving the embedded <c>ResultDisplayView</c>.</summary>
|
||||
public ResultDisplayViewModel Detail { get; }
|
||||
|
||||
// ── Observable state ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Newest-first list of captured completions for this session.</summary>
|
||||
public ObservableCollection<CompletedTestRun> History { get; } = new();
|
||||
|
||||
/// <summary>Currently selected history entry, or null when the list is empty.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(ExportPdfCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(RemoveEntryCommand))]
|
||||
private CompletedTestRun? _selectedRun;
|
||||
|
||||
/// <summary>True while the history list has no entries — drives the empty-state UI.</summary>
|
||||
public bool IsHistoryEmpty => History.Count == 0;
|
||||
|
||||
/// <summary>True when at least one entry exists — drives the Clear Session button's IsEnabled.</summary>
|
||||
public bool HasAnyEntries => History.Count > 0;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Initialises the Results page VM with the services it needs.</summary>
|
||||
public ResultsPageViewModel(
|
||||
MainViewModel root,
|
||||
IPdfService pdfService,
|
||||
IConfigurationService configService,
|
||||
ILocalizationService localizationService,
|
||||
IAppLogger logger)
|
||||
{
|
||||
Root = root;
|
||||
_pdf = pdfService;
|
||||
_config = configService;
|
||||
_loc = localizationService;
|
||||
_log = logger;
|
||||
|
||||
Detail = new ResultDisplayViewModel(localizationService);
|
||||
History.CollectionChanged += (_, _) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(IsHistoryEmpty));
|
||||
OnPropertyChanged(nameof(HasAnyEntries));
|
||||
ClearHistoryCommand.NotifyCanExecuteChanged();
|
||||
};
|
||||
}
|
||||
|
||||
// ── Capture ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Records a completed test session. Called from
|
||||
/// <see cref="MainViewModel"/>'s <c>OnTestFinished</c> hook on the UI thread.
|
||||
/// Captures a deep-cloned snapshot so that subsequent runs do not mutate
|
||||
/// prior entries in place.
|
||||
/// </summary>
|
||||
public void CaptureRun(PumpDefinition pump, bool interrupted, bool success)
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = CloneForHistory(pump);
|
||||
var entry = new CompletedTestRun
|
||||
{
|
||||
CompletedAt = DateTime.Now,
|
||||
PumpModel = pump.Model,
|
||||
PumpSerial = pump.SerialNumber,
|
||||
Interrupted = interrupted,
|
||||
OverallPassed = !interrupted && success && EvaluatePassed(snapshot.Tests),
|
||||
PumpSnapshot = snapshot,
|
||||
};
|
||||
History.Insert(0, entry);
|
||||
SelectedRun = entry;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"CaptureRun: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedRunChanged(CompletedTestRun? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
Detail.Clear();
|
||||
return;
|
||||
}
|
||||
Detail.LoadAllResults(value.PumpSnapshot.Tests);
|
||||
}
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Authenticates the operator, collects report details, then writes the PDF.</summary>
|
||||
[RelayCommand(CanExecute = nameof(CanExport))]
|
||||
private void ExportPdf()
|
||||
{
|
||||
if (SelectedRun == null) return;
|
||||
|
||||
// Step 1: Authenticate operator (same flow as Test page report export).
|
||||
var authVm = new UserCheckViewModel(_config, _loc, _lastAuthenticatedUser);
|
||||
var authDlg = new UserCheckDialog(authVm) { Owner = Application.Current.MainWindow };
|
||||
authDlg.ShowDialog();
|
||||
if (!authVm.Accepted) return;
|
||||
_lastAuthenticatedUser = authVm.AuthenticatedUser;
|
||||
|
||||
// Step 2: Collect report details, pre-filling observations from the captured run.
|
||||
var reportVm = new ReportViewModel(_config)
|
||||
{
|
||||
OperatorName = authVm.AuthenticatedUser,
|
||||
Observations = SelectedRun.Observations,
|
||||
};
|
||||
var reportDlg = new ReportDialog(reportVm) { Owner = Application.Current.MainWindow };
|
||||
reportDlg.ShowDialog();
|
||||
if (!reportVm.Accepted) return;
|
||||
|
||||
SelectedRun.Observations = reportVm.Observations;
|
||||
|
||||
try
|
||||
{
|
||||
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
string path = _pdf.GenerateReport(
|
||||
SelectedRun.PumpSnapshot,
|
||||
reportVm.OperatorName,
|
||||
reportVm.SelectedClientName,
|
||||
desktop,
|
||||
clientInfo: reportVm.ClientInfo,
|
||||
observations: reportVm.Observations);
|
||||
_log.Info(LogId, $"Report saved: {path}");
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path)
|
||||
{ UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"ExportPdf: {ex.Message}");
|
||||
MessageBox.Show(
|
||||
string.Format(_loc.GetString("Error.ReportGeneration"), ex.Message),
|
||||
_loc.GetString("Error.ReportTitle"),
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanExport() => SelectedRun != null;
|
||||
|
||||
/// <summary>Removes a single history entry by <see cref="CompletedTestRun.Id"/>.</summary>
|
||||
[RelayCommand(CanExecute = nameof(CanRemoveEntry))]
|
||||
private void RemoveEntry(Guid id)
|
||||
{
|
||||
var target = History.FirstOrDefault(e => e.Id == id);
|
||||
if (target == null) return;
|
||||
|
||||
bool wasSelected = ReferenceEquals(target, SelectedRun);
|
||||
History.Remove(target);
|
||||
if (wasSelected) SelectedRun = History.FirstOrDefault();
|
||||
}
|
||||
|
||||
private bool CanRemoveEntry(Guid id) => History.Any(e => e.Id == id);
|
||||
|
||||
/// <summary>Clears the whole session history after operator confirmation.</summary>
|
||||
[RelayCommand(CanExecute = nameof(HasAnyEntries))]
|
||||
private void ClearHistory()
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
_loc.GetString("Results.ClearSessionConfirm"),
|
||||
_loc.GetString("Results.ClearSessionButton"),
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
History.Clear();
|
||||
SelectedRun = null;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Builds a minimal deep copy of <paramref name="pump"/> that carries the
|
||||
/// fields consumed by <see cref="IPdfService"/> and the detail view, with
|
||||
/// <see cref="TestDefinition"/> entries deep-cloned via XML round-trip so
|
||||
/// results cannot be mutated by subsequent runs.
|
||||
/// </summary>
|
||||
private static PumpDefinition CloneForHistory(PumpDefinition pump)
|
||||
{
|
||||
var clone = new PumpDefinition
|
||||
{
|
||||
Id = pump.Id,
|
||||
Model = pump.Model,
|
||||
SerialNumber = pump.SerialNumber,
|
||||
Injector = pump.Injector,
|
||||
Tube = pump.Tube,
|
||||
Valve = pump.Valve,
|
||||
Tension = pump.Tension,
|
||||
Info = pump.Info,
|
||||
EcuText = pump.EcuText,
|
||||
Chaveta = pump.Chaveta,
|
||||
LockAngle = pump.LockAngle,
|
||||
LockAngleResult = pump.LockAngleResult,
|
||||
HasPreInjection = pump.HasPreInjection,
|
||||
Is4Cylinder = pump.Is4Cylinder,
|
||||
UnlockType = pump.UnlockType,
|
||||
Rotation = pump.Rotation,
|
||||
KwpVersion = pump.KwpVersion,
|
||||
};
|
||||
|
||||
foreach (var kv in pump.KlineInfo)
|
||||
clone.KlineInfo[kv.Key] = kv.Value;
|
||||
|
||||
foreach (var test in pump.Tests)
|
||||
{
|
||||
XElement xml = test.ToXml();
|
||||
clone.Tests.Add(TestDefinition.FromXml(xml));
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static bool EvaluatePassed(IReadOnlyList<TestDefinition> tests)
|
||||
{
|
||||
bool anyEvaluated = false;
|
||||
foreach (var t in tests)
|
||||
{
|
||||
foreach (var p in t.Phases)
|
||||
{
|
||||
if (!p.Enabled || p.Receives == null) continue;
|
||||
foreach (var tp in p.Receives)
|
||||
{
|
||||
if (tp.Result == null) continue;
|
||||
anyEvaluated = true;
|
||||
if (!tp.Result.Passed) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return anyEvaluated;
|
||||
}
|
||||
}
|
||||
}
|
||||
325
ViewModels/Pages/SettingsPageViewModel.cs
Normal file
325
ViewModels/Pages/SettingsPageViewModel.cs
Normal file
@@ -0,0 +1,325 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System.Windows;
|
||||
using HC_APTBS.Infrastructure.Kwp;
|
||||
using HC_APTBS.Models;
|
||||
using HC_APTBS.Services;
|
||||
using HC_APTBS.ViewModels.Dialogs;
|
||||
using HC_APTBS.Views.Dialogs;
|
||||
|
||||
namespace HC_APTBS.ViewModels.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the Settings navigation page.
|
||||
/// Loads a local copy of every <see cref="AppSettings"/> property so that
|
||||
/// Discard reverts all pending changes without touching persisted state.
|
||||
/// </summary>
|
||||
public sealed partial class SettingsPageViewModel : ObservableObject
|
||||
{
|
||||
private readonly IConfigurationService _config;
|
||||
private readonly ILocalizationService _loc;
|
||||
|
||||
/// <summary>
|
||||
/// Raised after <see cref="SaveCommand"/> successfully persists settings.
|
||||
/// The shell subscribes to reseed timers or other settings-dependent state.
|
||||
/// </summary>
|
||||
public event Action? SettingsSaved;
|
||||
|
||||
// ── Collections ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Available language codes for the language dropdown.</summary>
|
||||
public ObservableCollection<string> AvailableLanguages { get; } = new() { "ESP", "ENG" };
|
||||
|
||||
/// <summary>RPM-voltage lookup table, editable via DataGrid.</summary>
|
||||
public ObservableCollection<RpmVoltageRelation> Relations { get; } = new();
|
||||
|
||||
/// <summary>Available FTDI device serial numbers for K-Line port selection.</summary>
|
||||
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||
|
||||
// ── General ───────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _selectedLanguage = "ESP";
|
||||
[ObservableProperty] private int _daysKeepLogs = 7;
|
||||
|
||||
// ── Safety ────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _tempMax = 45;
|
||||
[ObservableProperty] private int _tempMin = 35;
|
||||
[ObservableProperty] private int _securityRpmLimit = 2500;
|
||||
[ObservableProperty] private int _maxPressureBar = 26;
|
||||
[ObservableProperty] private double _toleranceUpExtension = 0.08;
|
||||
[ObservableProperty] private double _tolerancePfpExtension = 0.1;
|
||||
[ObservableProperty] private bool _defaultIgnoreTin = true;
|
||||
|
||||
// ── PID ───────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private double _pidP = 0.1;
|
||||
[ObservableProperty] private double _pidI = 0.1;
|
||||
[ObservableProperty] private double _pidD = 0.04;
|
||||
[ObservableProperty] private int _pidLoopMs = 250;
|
||||
|
||||
// ── Motor ─────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _encoderResolution = 4096;
|
||||
[ObservableProperty] private double _voltageForMaxRpm = 10;
|
||||
[ObservableProperty] private int _maxRpm = 2500;
|
||||
[ObservableProperty] private bool _rightRelayValue = true;
|
||||
|
||||
// ── Company ───────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _companyName = string.Empty;
|
||||
[ObservableProperty] private string _companyInfo = string.Empty;
|
||||
[ObservableProperty] private string _reportLogoPath = string.Empty;
|
||||
|
||||
// ── K-Line ────────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _selectedKLinePort = string.Empty;
|
||||
|
||||
// ── Advanced (refresh intervals) ──────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private int _refreshBenchInterfaceMs = 20;
|
||||
[ObservableProperty] private int _refreshWhileReadingMs = 1500;
|
||||
[ObservableProperty] private int _refreshCanBusReadMs = 2;
|
||||
[ObservableProperty] private int _refreshPumpRequestMs = 250;
|
||||
[ObservableProperty] private int _refreshPumpParamsMs = 4;
|
||||
[ObservableProperty] private int _blinkIntervalMs = 1000;
|
||||
[ObservableProperty] private int _flasherIntervalMs = 800;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────
|
||||
|
||||
/// <param name="configService">Configuration service for loading/saving settings.</param>
|
||||
/// <param name="localizationService">Localization service for language switching.</param>
|
||||
public SettingsPageViewModel(IConfigurationService configService, ILocalizationService localizationService)
|
||||
{
|
||||
_config = configService;
|
||||
_loc = localizationService;
|
||||
|
||||
LoadFromConfig();
|
||||
EnumerateFtdiDevices();
|
||||
}
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Copies all local values back to AppSettings, saves to disk, applies language if changed.</summary>
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
var s = _config.Settings;
|
||||
|
||||
// General
|
||||
s.DaysKeepLogs = DaysKeepLogs;
|
||||
|
||||
// Safety
|
||||
s.TempMax = TempMax;
|
||||
s.TempMin = TempMin;
|
||||
s.SecurityRpmLimit = SecurityRpmLimit;
|
||||
s.MaxPressureBar = MaxPressureBar;
|
||||
s.ToleranceUpExtension = ToleranceUpExtension;
|
||||
s.TolerancePfpExtension = TolerancePfpExtension;
|
||||
s.DefaultIgnoreTin = DefaultIgnoreTin;
|
||||
|
||||
// PID
|
||||
s.PidP = PidP;
|
||||
s.PidI = PidI;
|
||||
s.PidD = PidD;
|
||||
s.PidLoopMs = PidLoopMs;
|
||||
|
||||
// Motor
|
||||
s.EncoderResolution = EncoderResolution;
|
||||
s.VoltageForMaxRpm = VoltageForMaxRpm;
|
||||
s.MaxRpm = MaxRpm;
|
||||
s.RightRelayValue = RightRelayValue;
|
||||
s.Relations = Relations.Select(r => new RpmVoltageRelation(r.Voltage, r.Rpm)).ToList();
|
||||
|
||||
// Company
|
||||
s.CompanyName = CompanyName;
|
||||
s.CompanyInfo = CompanyInfo;
|
||||
s.ReportLogoPath = ReportLogoPath;
|
||||
|
||||
// K-Line
|
||||
s.KLinePort = SelectedKLinePort;
|
||||
|
||||
// Advanced
|
||||
s.RefreshBenchInterfaceMs = RefreshBenchInterfaceMs;
|
||||
s.RefreshWhileReadingMs = RefreshWhileReadingMs;
|
||||
s.RefreshCanBusReadMs = RefreshCanBusReadMs;
|
||||
s.RefreshPumpRequestMs = RefreshPumpRequestMs;
|
||||
s.RefreshPumpParamsMs = RefreshPumpParamsMs;
|
||||
s.BlinkIntervalMs = BlinkIntervalMs;
|
||||
s.FlasherIntervalMs = FlasherIntervalMs;
|
||||
|
||||
// Language — switch if changed (also persists via LocalizationService)
|
||||
if (SelectedLanguage != _loc.CurrentLanguage)
|
||||
_loc.SetLanguage(SelectedLanguage);
|
||||
|
||||
_config.SaveSettings();
|
||||
|
||||
SettingsSaved?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Reverts all local fields to the currently persisted <see cref="AppSettings"/> values.</summary>
|
||||
[RelayCommand]
|
||||
private void Discard()
|
||||
{
|
||||
LoadFromConfig();
|
||||
}
|
||||
|
||||
/// <summary>Opens a file dialog to select a company logo image.</summary>
|
||||
[RelayCommand]
|
||||
private void BrowseLogo()
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = _loc.GetString("Dialog.Settings.BrowseLogoTitle"),
|
||||
Filter = "Image files|*.png;*.jpg;*.jpeg;*.bmp|All files|*.*"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(ReportLogoPath))
|
||||
{
|
||||
try { dlg.InitialDirectory = System.IO.Path.GetDirectoryName(ReportLogoPath); }
|
||||
catch { /* ignore invalid path */ }
|
||||
}
|
||||
|
||||
if (dlg.ShowDialog() == true)
|
||||
ReportLogoPath = dlg.FileName;
|
||||
}
|
||||
|
||||
/// <summary>Re-enumerates connected FTDI devices into <see cref="AvailablePorts"/>.</summary>
|
||||
[RelayCommand]
|
||||
private void RefreshPorts()
|
||||
{
|
||||
EnumerateFtdiDevices();
|
||||
}
|
||||
|
||||
/// <summary>Appends a new empty row to the RPM-voltage relation table.</summary>
|
||||
[RelayCommand]
|
||||
private void AddRelation()
|
||||
{
|
||||
Relations.Add(new RpmVoltageRelation(0.0, 0));
|
||||
}
|
||||
|
||||
/// <summary>Removes the selected row from the RPM-voltage relation table.</summary>
|
||||
[RelayCommand]
|
||||
private void RemoveRelation(RpmVoltageRelation? relation)
|
||||
{
|
||||
if (relation != null)
|
||||
Relations.Remove(relation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the user management dialog after a successful admin authentication.
|
||||
/// The auth dialog is re-prompted on every invocation — there is no session cache.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ManageUsers()
|
||||
{
|
||||
var owner = GetOwnerWindow();
|
||||
|
||||
// Step 1: admin authentication.
|
||||
var authVm = new UserCheckViewModel(_config, _loc);
|
||||
var authDlg = new UserCheckDialog(authVm) { Owner = owner };
|
||||
authDlg.ShowDialog();
|
||||
if (!authVm.Accepted) return;
|
||||
|
||||
// Step 2: management dialog.
|
||||
var manageVm = new UserManageViewModel(_config, _loc);
|
||||
var manageDlg = new UserManageDialog(manageVm) { Owner = owner };
|
||||
manageDlg.ShowDialog();
|
||||
}
|
||||
|
||||
/// <summary>Finds a plausible dialog owner (active window, else main window).</summary>
|
||||
private static Window? GetOwnerWindow()
|
||||
{
|
||||
foreach (Window w in Application.Current.Windows)
|
||||
{
|
||||
if (w.IsActive) return w;
|
||||
}
|
||||
return Application.Current.MainWindow;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Copies every persisted setting into the local mirror fields.</summary>
|
||||
private void LoadFromConfig()
|
||||
{
|
||||
var s = _config.Settings;
|
||||
|
||||
// General
|
||||
SelectedLanguage = s.Language;
|
||||
DaysKeepLogs = s.DaysKeepLogs;
|
||||
|
||||
// Safety
|
||||
TempMax = s.TempMax;
|
||||
TempMin = s.TempMin;
|
||||
SecurityRpmLimit = s.SecurityRpmLimit;
|
||||
MaxPressureBar = s.MaxPressureBar;
|
||||
ToleranceUpExtension = s.ToleranceUpExtension;
|
||||
TolerancePfpExtension = s.TolerancePfpExtension;
|
||||
DefaultIgnoreTin = s.DefaultIgnoreTin;
|
||||
|
||||
// PID
|
||||
PidP = s.PidP;
|
||||
PidI = s.PidI;
|
||||
PidD = s.PidD;
|
||||
PidLoopMs = s.PidLoopMs;
|
||||
|
||||
// Motor
|
||||
EncoderResolution = s.EncoderResolution;
|
||||
VoltageForMaxRpm = s.VoltageForMaxRpm;
|
||||
MaxRpm = s.MaxRpm;
|
||||
RightRelayValue = s.RightRelayValue;
|
||||
|
||||
// Company
|
||||
CompanyName = s.CompanyName;
|
||||
CompanyInfo = s.CompanyInfo;
|
||||
ReportLogoPath = s.ReportLogoPath;
|
||||
|
||||
// K-Line
|
||||
SelectedKLinePort = s.KLinePort;
|
||||
|
||||
// Advanced
|
||||
RefreshBenchInterfaceMs = s.RefreshBenchInterfaceMs;
|
||||
RefreshWhileReadingMs = s.RefreshWhileReadingMs;
|
||||
RefreshCanBusReadMs = s.RefreshCanBusReadMs;
|
||||
RefreshPumpRequestMs = s.RefreshPumpRequestMs;
|
||||
RefreshPumpParamsMs = s.RefreshPumpParamsMs;
|
||||
BlinkIntervalMs = s.BlinkIntervalMs;
|
||||
FlasherIntervalMs = s.FlasherIntervalMs;
|
||||
|
||||
// Deep-copy the RPM-voltage relation table
|
||||
Relations.Clear();
|
||||
foreach (var r in s.Relations)
|
||||
Relations.Add(new RpmVoltageRelation(r.Voltage, r.Rpm));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates <see cref="AvailablePorts"/> with serial numbers of connected
|
||||
/// FTDI devices. Fails silently if the FTDI driver DLL is not present.
|
||||
/// </summary>
|
||||
private void EnumerateFtdiDevices()
|
||||
{
|
||||
AvailablePorts.Clear();
|
||||
try
|
||||
{
|
||||
uint count = FtdiInterface.GetDevicesCount();
|
||||
if (count == 0) return;
|
||||
|
||||
var list = new FT_DEVICE_INFO_NODE[count];
|
||||
FtdiInterface.GetDeviceList(list);
|
||||
|
||||
foreach (var device in list)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(device.SerialNumber))
|
||||
AvailablePorts.Add(device.SerialNumber);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// FTDI DLL not loaded or no devices — leave list empty.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
ViewModels/Pages/TestsPageViewModel.cs
Normal file
233
ViewModels/Pages/TestsPageViewModel.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user