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

262 lines
11 KiB
C#

using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels
{
/// <summary>
/// Root ViewModel for the test panel that displays all tests for the selected pump.
///
/// <para>
/// Replaces the former <c>TestDisplayViewModel</c>. Holds one
/// <see cref="TestSectionViewModel"/> per <see cref="TestDefinition"/> and provides
/// the public API that <see cref="MainViewModel"/> calls in response to
/// <see cref="Services.IBenchService"/> events.
/// </para>
/// </summary>
public sealed partial class TestPanelViewModel : ObservableObject
{
private readonly ILocalizationService _loc;
/// <summary>Initialises a new test panel with a localization service.</summary>
public TestPanelViewModel(ILocalizationService loc) => _loc = loc;
// ── Cached active phase for fast live-indicator lookup ─────────────────────
private PhaseCardViewModel? _activePhaseCard;
// ── Global toggles ────────────────────────────────────────────────────────
/// <summary>
/// Controls visibility of operation values (RPM, ME, FBKW) on all phase cards.
/// Cascades to every <see cref="PhaseCardViewModel.ShowOperationValues"/>.
/// </summary>
[ObservableProperty] private bool _showOperationValues;
// ── Status ────────────────────────────────────────────────────────────────
/// <summary>Current verbose status message from the bench service.</summary>
[ObservableProperty] private string _statusText = string.Empty;
/// <summary>True while a test sequence is in progress.</summary>
[ObservableProperty] private bool _isRunning;
/// <summary>Estimated remaining time for the entire test sequence (seconds).</summary>
[ObservableProperty] private int _remainingSeconds;
// ── Active phase countdown (driven by IBenchService.PhaseTimerTick) ───────
/// <summary>Name of the currently running phase (empty when idle).</summary>
[ObservableProperty] private string _currentPhaseName = string.Empty;
/// <summary>Sub-section of the current phase: "Conditioning", "Measuring", or empty.</summary>
[ObservableProperty] private string _sectionLabel = string.Empty;
/// <summary>Seconds remaining in the current sub-section countdown.</summary>
[ObservableProperty] private int _phaseRemainingSeconds;
/// <summary>Total seconds for the current sub-section (denominator for progress).</summary>
[ObservableProperty] private int _phaseTotalSeconds;
/// <summary>Progress through the current sub-section (0.0 → 1.0).</summary>
[ObservableProperty] private double _phaseProgress;
// ── Test sections ─────────────────────────────────────────────────────────
/// <summary>All test sections for the currently loaded pump.</summary>
public ObservableCollection<TestSectionViewModel> Tests { get; } = new();
// ── Show values cascade ───────────────────────────────────────────────────
partial void OnShowOperationValuesChanged(bool value)
{
foreach (var section in Tests)
foreach (var phase in section.Phases)
phase.ShowOperationValues = value;
}
// ── Commands ──────────────────────────────────────────────────────────────
/// <summary>
/// Toggles enable/disable for every phase across all test sections.
/// If any phase is currently disabled, enables all; otherwise disables all.
/// </summary>
[RelayCommand]
private void ToggleCheckAll()
{
bool anyDisabled = Tests.Any(s => s.Phases.Any(p => !p.IsEnabled));
bool newState = anyDisabled;
foreach (var section in Tests)
{
// Bypass per-section cascade guard by setting AllPhasesChecked directly,
// which will cascade down to children.
section.AllPhasesChecked = newState;
}
}
// ── Public API: loading ───────────────────────────────────────────────────
/// <summary>
/// Populates the test panel with all tests from the given pump definition.
/// Call when the selected pump changes.
/// </summary>
/// <param name="pump">The pump whose tests to display.</param>
public void LoadAllTests(PumpDefinition pump)
{
Tests.Clear();
_activePhaseCard = null;
StatusText = string.Empty;
RemainingSeconds = 0;
foreach (var testDef in pump.Tests)
{
var section = TestSectionViewModel.FromDefinition(testDef, ShowOperationValues, _loc);
Tests.Add(section);
}
// Compute initial remaining seconds estimate.
RemainingSeconds = pump.Tests.Sum(t => t.EstimatedTotalSeconds());
}
// ── Public API: real-time updates from BenchService events ─────────────────
/// <summary>
/// Marks the named phase as actively executing and clears any previous active state.
/// Caches the active phase card for fast live-indicator updates.
/// </summary>
/// <param name="phaseName">Name of the phase that is now running.</param>
public void SetActivePhase(string phaseName)
{
StatusText = phaseName;
CurrentPhaseName = phaseName;
ClearPhaseTimer();
_activePhaseCard = null;
foreach (var section in Tests)
{
bool sectionActive = false;
foreach (var phase in section.Phases)
{
if (phase.Name == phaseName && !phase.IsPassed && !phase.IsFailed)
{
phase.IsActive = true;
_activePhaseCard = phase;
sectionActive = true;
}
else
{
phase.IsActive = false;
}
}
section.IsActiveTest = sectionActive;
}
}
/// <summary>
/// Marks a phase as completed with the given pass/fail result.
/// </summary>
/// <param name="phaseName">Name of the completed phase.</param>
/// <param name="passed">True if the phase passed all criteria.</param>
public void SetPhaseResult(string phaseName, bool passed)
{
foreach (var section in Tests)
{
foreach (var phase in section.Phases)
{
if (phase.Name != phaseName || (!phase.IsActive && !phase.IsPassed && !phase.IsFailed))
continue;
// Only update if this is the active phase (avoid overwriting already-completed phases
// with the same name in different tests).
if (!phase.IsActive) continue;
phase.IsActive = false;
phase.IsPassed = passed;
phase.IsFailed = !passed;
phase.ResultText = passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
break;
}
// Recalculate section active state.
section.IsActiveTest = section.Phases.Any(p => p.IsActive);
}
if (_activePhaseCard?.Name == phaseName)
_activePhaseCard = null;
}
/// <summary>
/// Updates the live measurement value on the graphic indicator for the currently
/// active phase that has a matching receive parameter.
/// </summary>
/// <param name="paramName">CAN parameter name (e.g. "QDelivery").</param>
/// <param name="value">Current measured value.</param>
public void UpdateLiveIndicator(string paramName, double value)
{
if (_activePhaseCard == null) return;
foreach (var indicator in _activePhaseCard.ResultIndicators)
{
if (indicator.ParameterName == paramName)
{
indicator.CurrentValue = value;
return;
}
}
}
/// <summary>
/// Applies a phase-timer tick from <see cref="IBenchService.PhaseTimerTick"/>.
/// Updates the sub-section label, remaining/total seconds and computed progress.
/// Must be called on the UI thread.
/// </summary>
public void ApplyPhaseTimerTick(string section, int remaining, int total)
{
SectionLabel = section;
PhaseRemainingSeconds = remaining;
PhaseTotalSeconds = total;
PhaseProgress = total > 0 ? 1.0 - (double)remaining / total : 0.0;
}
/// <summary>Clears the active-phase countdown (call on phase change and test end).</summary>
public void ClearPhaseTimer()
{
SectionLabel = string.Empty;
PhaseRemainingSeconds = 0;
PhaseTotalSeconds = 0;
PhaseProgress = 0;
}
/// <summary>
/// Resets all phase execution states and graphic indicators for a fresh test run.
/// </summary>
public void ResetResults()
{
_activePhaseCard = null;
StatusText = string.Empty;
CurrentPhaseName = string.Empty;
ClearPhaseTimer();
foreach (var section in Tests)
{
section.IsActiveTest = false;
foreach (var phase in section.Phases)
phase.Reset();
}
// Recalculate remaining seconds.
int total = 0;
foreach (var section in Tests)
{
if (section.Source != null)
total += section.Source.EstimatedTotalSeconds();
}
RemainingSeconds = total;
}
}
}