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>
274 lines
12 KiB
C#
274 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|