diff --git a/MainWindow.xaml b/MainWindow.xaml
index 5887615..a76168d 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -451,8 +451,6 @@
-
-
@@ -509,42 +507,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Models/BenchConfiguration.cs b/Models/BenchConfiguration.cs
index 565beb4..e884e14 100644
--- a/Models/BenchConfiguration.cs
+++ b/Models/BenchConfiguration.cs
@@ -135,6 +135,12 @@ namespace HC_APTBS.Models
/// Absolute path to the company logo image for the report.
public string ReportLogoPath { get; set; } = string.Empty;
+ ///
+ /// Comma-separated user:password credential pairs for operator authentication
+ /// before report generation.
+ ///
+ public string Users { get; set; } = "admin:admin";
+
// ── K-Line port ───────────────────────────────────────────────────────
/// Serial port or FTDI device identifier for the K-Line interface.
diff --git a/Services/IConfigurationService.cs b/Services/IConfigurationService.cs
index be76146..a35892a 100644
--- a/Services/IConfigurationService.cs
+++ b/Services/IConfigurationService.cs
@@ -62,5 +62,16 @@ namespace HC_APTBS.Services
/// Saves updated sensor calibration data to sensors.xml.
void SaveSensors();
+
+ // ── Users ─────────────────────────────────────────────────────────────────
+
+ /// Validates a username/password pair against stored credentials.
+ bool ValidateUser(string username, string password);
+
+ /// Returns all stored user credentials as a dictionary.
+ IReadOnlyDictionary GetUsers();
+
+ /// Replaces all stored user credentials and persists them.
+ void UpdateUsers(Dictionary users);
}
}
diff --git a/Services/Impl/ConfigurationService.cs b/Services/Impl/ConfigurationService.cs
index 02021bd..115a4ab 100644
--- a/Services/Impl/ConfigurationService.cs
+++ b/Services/Impl/ConfigurationService.cs
@@ -94,7 +94,8 @@ namespace HC_APTBS.Services.Impl
new XElement("ReportLogoPath", Settings.ReportLogoPath),
new XElement("KLinePort", Settings.KLinePort),
new XElement("Language", Settings.Language),
- new XElement("Relations", RpmVoltageRelation.Serialise(Settings.Relations))
+ new XElement("Relations", RpmVoltageRelation.Serialise(Settings.Relations)),
+ new XElement("Users", Settings.Users)
);
new XDocument(root).Save(ConfigXml);
SaveSensors();
@@ -355,6 +356,7 @@ namespace HC_APTBS.Services.Impl
TryString(r, "KLinePort", v => _settings.KLinePort = v);
TryString(r, "Language", v => _settings.Language = v);
TryString(r, "Relations", v => _settings.Relations = RpmVoltageRelation.Deserialise(v));
+ TryString(r, "Users", v => _settings.Users = v);
}
catch (Exception ex)
{
@@ -640,6 +642,46 @@ namespace HC_APTBS.Services.Impl
try { var v = root.Element(name)?.Value; if (v != null) assign(v); }
catch { }
}
+
+ // ── Users ─────────────────────────────────────────────────────────────────
+
+ ///
+ public bool ValidateUser(string username, string password)
+ {
+ if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
+ return false;
+
+ string check = username + ":" + password;
+ foreach (string entry in Settings.Users.Split(','))
+ {
+ if (entry == check) return true;
+ }
+ return false;
+ }
+
+ ///
+ public IReadOnlyDictionary GetUsers()
+ {
+ var dict = new Dictionary();
+ foreach (string kv in Settings.Users.Split(','))
+ {
+ string[] parts = kv.Split(':');
+ if (parts.Length == 2 && parts[0].Length > 0)
+ dict[parts[0]] = parts[1];
+ }
+ return dict;
+ }
+
+ ///
+ public void UpdateUsers(Dictionary users)
+ {
+ var entries = new List(users.Count);
+ foreach (var kv in users)
+ entries.Add(kv.Key + ":" + kv.Value);
+
+ Settings.Users = string.Join(",", entries);
+ SaveSettings();
+ }
}
// ── XPath extension shim ──────────────────────────────────────────────────────
diff --git a/ViewModels/Dialogs/UserCheckViewModel.cs b/ViewModels/Dialogs/UserCheckViewModel.cs
new file mode 100644
index 0000000..28f7e26
--- /dev/null
+++ b/ViewModels/Dialogs/UserCheckViewModel.cs
@@ -0,0 +1,81 @@
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using HC_APTBS.Services;
+
+namespace HC_APTBS.ViewModels.Dialogs
+{
+ ///
+ /// ViewModel for the user authentication dialog shown before report generation.
+ /// Validates operator credentials against the stored user list.
+ ///
+ public sealed partial class UserCheckViewModel : ObservableObject
+ {
+ private readonly IConfigurationService _config;
+
+ /// Initialises the dialog, optionally pre-filling the last used username.
+ public UserCheckViewModel(IConfigurationService config, string lastUsername = "")
+ {
+ _config = config;
+ _username = lastUsername;
+ }
+
+ // ── Bindable properties ───────────────────────────────────────────────────
+
+ /// Username entered by the operator.
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
+ private string _username = string.Empty;
+
+ /// Password entered by the operator (set from code-behind PasswordChanged handler).
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
+ private string _password = string.Empty;
+
+ // ── Dialog result ─────────────────────────────────────────────────────────
+
+ /// True if the user authenticated successfully.
+ public bool Accepted { get; private set; }
+
+ /// The validated username, available after is true.
+ public string AuthenticatedUser { get; private set; } = string.Empty;
+
+ // ── Commands ──────────────────────────────────────────────────────────────
+
+ /// Validates credentials and closes the dialog on success.
+ [RelayCommand(CanExecute = nameof(CanAccept))]
+ private void Accept()
+ {
+ if (_config.ValidateUser(Username, Password))
+ {
+ AuthenticatedUser = Username;
+ Accepted = true;
+ RequestClose?.Invoke();
+ }
+ else
+ {
+ MessageBox.Show(
+ "Invalid username or password.\n(Both are case-sensitive.)",
+ "Authentication Error",
+ MessageBoxButton.OK,
+ MessageBoxImage.Stop);
+ }
+ }
+
+ private bool CanAccept() => !string.IsNullOrWhiteSpace(Username)
+ && !string.IsNullOrEmpty(Password);
+
+ /// Cancels authentication and closes the dialog.
+ [RelayCommand]
+ private void Cancel()
+ {
+ Accepted = false;
+ RequestClose?.Invoke();
+ }
+
+ // ── Events ────────────────────────────────────────────────────────────────
+
+ /// Raised when the dialog should close itself.
+ public event System.Action? RequestClose;
+ }
+}
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 5e2c962..d2f40ae 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -9,6 +9,8 @@ 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
{
@@ -43,6 +45,9 @@ namespace HC_APTBS.ViewModels
private CancellationTokenSource? _testCts;
+ /// Remembers the last authenticated username to pre-fill the next auth dialog.
+ private string _lastAuthenticatedUser = string.Empty;
+
// ── Child ViewModels ──────────────────────────────────────────────────────
/// ViewModel for pump selection and K-Line ECU identification.
@@ -331,14 +336,6 @@ namespace HC_APTBS.ViewModels
/// Verbose status message from bench/test operations.
[ObservableProperty] private string _verboseStatus = string.Empty;
- // ── Operator / client info ────────────────────────────────────────────────
-
- /// Operator name for report generation.
- [ObservableProperty] private string _operatorName = string.Empty;
-
- /// Client name for report generation.
- [ObservableProperty] private string _clientName = string.Empty;
-
// ── Test saved state ──────────────────────────────────────────────────────
/// True when the current test results have been saved to a report.
@@ -407,14 +404,28 @@ namespace HC_APTBS.ViewModels
private void GenerateReport()
{
if (CurrentPump == null) return;
+
+ // Step 1: Authenticate operator.
+ var authVm = new UserCheckViewModel(_config, _lastAuthenticatedUser);
+ var authDlg = new UserCheckDialog(authVm) { Owner = Application.Current.MainWindow };
+ authDlg.ShowDialog();
+ if (!authVm.Accepted) return;
+ _lastAuthenticatedUser = authVm.AuthenticatedUser;
+
+ // Step 2: Collect report details (client, company, observations).
+ var reportVm = new ReportViewModel(_config) { OperatorName = authVm.AuthenticatedUser };
+ var reportDlg = new ReportDialog(reportVm) { Owner = Application.Current.MainWindow };
+ reportDlg.ShowDialog();
+ if (!reportVm.Accepted) return;
+
try
{
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
- string path = _pdf.GenerateReport(CurrentPump, OperatorName, ClientName, desktop);
+ string path = _pdf.GenerateReport(
+ CurrentPump, reportVm.OperatorName, reportVm.SelectedClientName, desktop);
_log.Info(LogId, $"Report saved: {path}");
IsTestSaved = true;
- // Open the generated PDF with the default viewer.
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path)
{ UseShellExecute = true });
}
diff --git a/Views/Dialogs/ReportDialog.xaml b/Views/Dialogs/ReportDialog.xaml
index 76c4ef7..54a598b 100644
--- a/Views/Dialogs/ReportDialog.xaml
+++ b/Views/Dialogs/ReportDialog.xaml
@@ -80,7 +80,8 @@
+ VerticalAlignment="Center" FontSize="13" Margin="4,0" Height="24"
+ IsReadOnly="True" Background="#F0F0F0"/>
diff --git a/Views/Dialogs/UserCheckDialog.xaml b/Views/Dialogs/UserCheckDialog.xaml
new file mode 100644
index 0000000..7de5b68
--- /dev/null
+++ b/Views/Dialogs/UserCheckDialog.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/Dialogs/UserCheckDialog.xaml.cs b/Views/Dialogs/UserCheckDialog.xaml.cs
new file mode 100644
index 0000000..5dc901f
--- /dev/null
+++ b/Views/Dialogs/UserCheckDialog.xaml.cs
@@ -0,0 +1,30 @@
+using System.Windows;
+using System.Windows.Controls;
+using HC_APTBS.ViewModels.Dialogs;
+
+namespace HC_APTBS.Views.Dialogs
+{
+ ///
+ /// Authentication dialog that collects username and password before report generation.
+ ///
+ public partial class UserCheckDialog : Window
+ {
+ /// Creates the dialog and wires the ViewModel.
+ public UserCheckDialog(UserCheckViewModel vm)
+ {
+ InitializeComponent();
+ DataContext = vm;
+ vm.RequestClose += Close;
+ }
+
+ ///
+ /// Forwards the PasswordBox value to the ViewModel.
+ /// WPF intentionally does not expose Password as a DependencyProperty.
+ ///
+ private void OnPasswordChanged(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is UserCheckViewModel vm)
+ vm.Password = ((PasswordBox)sender).Password;
+ }
+ }
+}
diff --git a/Views/UserControls/TestPanelView.xaml b/Views/UserControls/TestPanelView.xaml
index 1edbbeb..55bd541 100644
--- a/Views/UserControls/TestPanelView.xaml
+++ b/Views/UserControls/TestPanelView.xaml
@@ -308,13 +308,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -347,14 +374,14 @@
-
-
+