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

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;
}
}
}