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>
180 lines
6.8 KiB
C#
180 lines
6.8 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Windows;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using HC_APTBS.Services;
|
|
using HC_APTBS.Views.Dialogs;
|
|
|
|
namespace HC_APTBS.ViewModels.Dialogs
|
|
{
|
|
/// <summary>
|
|
/// ViewModel for the user management dialog.
|
|
/// Invokes incremental methods on <see cref="IConfigurationService"/>
|
|
/// (<c>AddUser</c>, <c>RemoveUser</c>, <c>ChangeUserPassword</c>) so that
|
|
/// hashes of untouched accounts are preserved. Each action persists immediately;
|
|
/// the dialog has no Accept/Cancel split — Close simply dismisses the window.
|
|
/// </summary>
|
|
public sealed partial class UserManageViewModel : ObservableObject
|
|
{
|
|
private readonly IConfigurationService _config;
|
|
private readonly ILocalizationService _loc;
|
|
|
|
/// <summary>Creates the ViewModel and populates <see cref="Users"/> from the service.</summary>
|
|
public UserManageViewModel(IConfigurationService config, ILocalizationService loc)
|
|
{
|
|
_config = config;
|
|
_loc = loc;
|
|
ReloadUsers();
|
|
}
|
|
|
|
// ── Bindable state ────────────────────────────────────────────────────
|
|
|
|
/// <summary>Usernames currently stored in the configuration.</summary>
|
|
public ObservableCollection<string> Users { get; } = new();
|
|
|
|
/// <summary>Currently selected username in the DataGrid, or null when nothing is selected.</summary>
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ChangePasswordCommand))]
|
|
private string? _selectedUser;
|
|
|
|
/// <summary>Raised to close the owning dialog window.</summary>
|
|
public event Action? RequestClose;
|
|
|
|
// ── Commands ──────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Prompts for a username and password, then adds the user via the service.</summary>
|
|
[RelayCommand]
|
|
private void Add()
|
|
{
|
|
var prompt = new UserPromptDialog(
|
|
_loc.GetString("Dialog.UserManage.Prompt.AddTitle"),
|
|
usernameVisible: true)
|
|
{
|
|
Owner = Application.Current?.Windows.Count > 0
|
|
? GetActiveWindow()
|
|
: null
|
|
};
|
|
|
|
if (prompt.ShowDialog() != true)
|
|
return;
|
|
|
|
string username = prompt.EnteredUsername?.Trim() ?? string.Empty;
|
|
string password = prompt.EnteredPassword ?? string.Empty;
|
|
|
|
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
|
{
|
|
ShowError("Dialog.UserManage.Error.Empty", "Dialog.UserManage.Error.EmptyTitle");
|
|
return;
|
|
}
|
|
if (username.IndexOfAny(new[] { ':', ',' }) >= 0)
|
|
{
|
|
ShowError("Dialog.UserManage.Error.InvalidChars", "Dialog.UserManage.Error.InvalidCharsTitle");
|
|
return;
|
|
}
|
|
|
|
if (!_config.AddUser(username, password))
|
|
{
|
|
ShowError("Dialog.UserManage.Error.Duplicate", "Dialog.UserManage.Error.DuplicateTitle");
|
|
return;
|
|
}
|
|
|
|
ReloadUsers();
|
|
SelectedUser = username;
|
|
}
|
|
|
|
/// <summary>Removes the selected user after confirmation, refusing when only one user remains.</summary>
|
|
[RelayCommand(CanExecute = nameof(HasSelection))]
|
|
private void Remove()
|
|
{
|
|
string user = SelectedUser!;
|
|
var confirm = MessageBox.Show(
|
|
string.Format(_loc.GetString("Dialog.UserManage.Confirm.Remove"), user),
|
|
_loc.GetString("Dialog.UserManage.Confirm.RemoveTitle"),
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Question);
|
|
|
|
if (confirm != MessageBoxResult.Yes)
|
|
return;
|
|
|
|
if (!_config.RemoveUser(user))
|
|
{
|
|
ShowError("Dialog.UserManage.Error.LastUser", "Dialog.UserManage.Error.LastUserTitle");
|
|
return;
|
|
}
|
|
|
|
ReloadUsers();
|
|
}
|
|
|
|
/// <summary>Prompts for a new password for the selected user and applies it.</summary>
|
|
[RelayCommand(CanExecute = nameof(HasSelection))]
|
|
private void ChangePassword()
|
|
{
|
|
string user = SelectedUser!;
|
|
|
|
var prompt = new UserPromptDialog(
|
|
string.Format(_loc.GetString("Dialog.UserManage.Prompt.ChangeTitle"), user),
|
|
usernameVisible: false,
|
|
prefillUsername: user)
|
|
{
|
|
Owner = GetActiveWindow()
|
|
};
|
|
|
|
if (prompt.ShowDialog() != true)
|
|
return;
|
|
|
|
string newPassword = prompt.EnteredPassword ?? string.Empty;
|
|
if (string.IsNullOrEmpty(newPassword))
|
|
{
|
|
ShowError("Dialog.UserManage.Error.Empty", "Dialog.UserManage.Error.EmptyTitle");
|
|
return;
|
|
}
|
|
|
|
if (!_config.ChangeUserPassword(user, newPassword))
|
|
{
|
|
ShowError("Dialog.UserManage.Error.Empty", "Dialog.UserManage.Error.EmptyTitle");
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>Closes the dialog.</summary>
|
|
[RelayCommand]
|
|
private void Close() => RequestClose?.Invoke();
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
private bool HasSelection() => !string.IsNullOrEmpty(SelectedUser);
|
|
|
|
/// <summary>Reloads the user list from the service, preserving selection when possible.</summary>
|
|
private void ReloadUsers()
|
|
{
|
|
string? previous = SelectedUser;
|
|
Users.Clear();
|
|
foreach (var name in _config.GetUsers())
|
|
Users.Add(name);
|
|
|
|
SelectedUser = (previous != null && Users.Contains(previous)) ? previous : null;
|
|
}
|
|
|
|
private void ShowError(string messageKey, string titleKey)
|
|
{
|
|
MessageBox.Show(
|
|
_loc.GetString(messageKey),
|
|
_loc.GetString(titleKey),
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Stop);
|
|
}
|
|
|
|
/// <summary>Returns the topmost active window for use as a dialog owner, or null if none.</summary>
|
|
private static Window? GetActiveWindow()
|
|
{
|
|
foreach (Window w in Application.Current.Windows)
|
|
{
|
|
if (w.IsActive) return w;
|
|
}
|
|
return Application.Current.MainWindow;
|
|
}
|
|
}
|
|
}
|