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 { /// /// ViewModel for the Results navigation page (§5 in docs/ui-structure.md). /// /// Owns a session-only history of completed test runs, drives the embedded /// detail pane, hosts the operator /// observations/notes field, and runs the PDF export flow against the selected /// snapshot. /// 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"; /// Remembers the last authenticated user to pre-fill the next auth dialog. private string _lastAuthenticatedUser = string.Empty; // ── Root reference ──────────────────────────────────────────────────────── /// Root ViewModel — lets page XAML bind app-global state when needed. public MainViewModel Root { get; } // ── Child VMs ───────────────────────────────────────────────────────────── /// Detail-pane ViewModel driving the embedded ResultDisplayView. public ResultDisplayViewModel Detail { get; } // ── Observable state ────────────────────────────────────────────────────── /// Newest-first list of captured completions for this session. public ObservableCollection History { get; } = new(); /// Currently selected history entry, or null when the list is empty. [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ExportPdfCommand))] [NotifyCanExecuteChangedFor(nameof(RemoveEntryCommand))] private CompletedTestRun? _selectedRun; /// True while the history list has no entries — drives the empty-state UI. public bool IsHistoryEmpty => History.Count == 0; /// True when at least one entry exists — drives the Clear Session button's IsEnabled. public bool HasAnyEntries => History.Count > 0; // ── Constructor ─────────────────────────────────────────────────────────── /// Initialises the Results page VM with the services it needs. 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 ─────────────────────────────────────────────────────────────── /// /// Records a completed test session. Called from /// 's OnTestFinished hook on the UI thread. /// Captures a deep-cloned snapshot so that subsequent runs do not mutate /// prior entries in place. /// 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 ────────────────────────────────────────────────────────────── /// Authenticates the operator, collects report details, then writes the PDF. [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; /// Removes a single history entry by . [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); /// Clears the whole session history after operator confirmation. [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 ─────────────────────────────────────────────────────────────── /// /// Builds a minimal deep copy of that carries the /// fields consumed by and the detail view, with /// entries deep-cloned via XML round-trip so /// results cannot be mutated by subsequent runs. /// 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 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; } } }