Replace DB-to-device page with device-to-device UI
Replace the old DB → Device page with the new Device → Device page. - Remove the old DbToDevice view and code-behind - Add the new DeviceToDevice view and code-behind - Add source/target device selection - Add connection checks, user loading, selection, progress, and sync action UImain
parent
78b8a9bc86
commit
b73476c5b3
|
|
@ -1,136 +0,0 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using HikvisionAttendanceManager.App.Models;
|
||||
using HikvisionAttendanceManager.App.Services;
|
||||
|
||||
namespace HikvisionAttendanceManager.App;
|
||||
|
||||
public partial class DbToDeviceView : UserControl
|
||||
{
|
||||
private readonly DeviceService _devices = new();
|
||||
private readonly AttendanceMachineUserRepository _machineUsers = new();
|
||||
private readonly AttendanceMachineFaceTemplateRepository _faceTemplates = new();
|
||||
private readonly TemplateSyncService _service;
|
||||
private readonly ObservableCollection<SelectableMachineUser> _rows = [];
|
||||
private bool _initialized;
|
||||
|
||||
public DbToDeviceView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_service = new TemplateSyncService(new HikvisionIsapiClient(), _machineUsers, _faceTemplates, new OperationHistoryService());
|
||||
UsersGrid.ItemsSource = _rows;
|
||||
UpdateUsersEmptyState();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
await RefreshAsync();
|
||||
}
|
||||
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
var originalContent = RefreshButton.Content;
|
||||
try
|
||||
{
|
||||
RefreshButton.IsEnabled = false;
|
||||
RefreshButton.Content = "Refreshing...";
|
||||
SourceCombo.SelectedItem = null;
|
||||
TargetCombo.SelectedItem = null;
|
||||
_rows.Clear();
|
||||
UpdateUsersEmptyState();
|
||||
ProgressCard.Visibility = Visibility.Collapsed;
|
||||
ProgressBar.Value = 0;
|
||||
ProgressText.Text = "";
|
||||
CounterText.Text = "";
|
||||
UsersGrid.Items.Refresh();
|
||||
var devices = await _devices.LoadUnifiedAsync();
|
||||
SourceCombo.ItemsSource = devices;
|
||||
TargetCombo.ItemsSource = devices;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RefreshButton.IsEnabled = true;
|
||||
RefreshButton.Content = originalContent;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Refresh_Click(object sender, RoutedEventArgs e) => await RefreshAsync();
|
||||
|
||||
private void SourceChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_rows.Clear();
|
||||
UpdateUsersEmptyState();
|
||||
}
|
||||
|
||||
private async void LoadUsers_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SourceCombo.SelectedItem is not Device source) { MessageBox.Show("Select a source machine."); return; }
|
||||
_rows.Clear();
|
||||
var users = await _machineUsers.GetActiveByMachineAsync(source.MachineId, CancellationToken.None);
|
||||
foreach (var user in users)
|
||||
{
|
||||
var template = await _faceTemplates.TryGetActiveAsync(user.SerialNumber, CancellationToken.None);
|
||||
var hasFace = template?.Template is { Length: > 0 } && HikvisionIsapiClient.IsUploadableFaceTemplate(template.Template);
|
||||
_rows.Add(new SelectableMachineUser(user.SerialNumber, user.EmployeeName, hasFace));
|
||||
}
|
||||
|
||||
UpdateUsersEmptyState();
|
||||
}
|
||||
|
||||
private void SelectAll_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
foreach (var row in _rows) row.IsSelected = true;
|
||||
UsersGrid.Items.Refresh();
|
||||
}
|
||||
|
||||
private void ClearSelection_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
foreach (var row in _rows) row.IsSelected = false;
|
||||
UsersGrid.Items.Refresh();
|
||||
}
|
||||
|
||||
private void UpdateUsersEmptyState()
|
||||
{
|
||||
UsersEmptyState.Visibility = _rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async void Transfer_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SourceCombo.SelectedItem is not Device source || TargetCombo.SelectedItem is not Device target)
|
||||
{
|
||||
MessageBox.Show("Select source and target machines.");
|
||||
return;
|
||||
}
|
||||
if (string.Equals(source.MachineId, target.MachineId, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(source.IpAddress, target.IpAddress, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
MessageBox.Show("Source and target must be different devices.");
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = _rows.Where(r => r.IsSelected).Select(r => r.SerialNumber).ToList();
|
||||
if (selected.Count == 0) { MessageBox.Show("Select at least one user."); return; }
|
||||
|
||||
ProgressCard.Visibility = Visibility.Visible;
|
||||
var progress = new Progress<UserSyncProgress>(p =>
|
||||
{
|
||||
ProgressBar.Maximum = Math.Max(1, p.Total);
|
||||
ProgressBar.Value = p.Completed;
|
||||
ProgressText.Text = $"{p.Completed}/{p.Total} · {p.CurrentEmployee} · {p.CurrentOperation}";
|
||||
CounterText.Text = $"Users: {p.Created} · Faces: {p.FacesUploaded} · Skipped: {p.Skipped} · Failed: {p.Failed}";
|
||||
});
|
||||
try
|
||||
{
|
||||
var entry = await _service.DbToDeviceAsync(source, target, selected, progress, CancellationToken.None);
|
||||
ProgressText.Text = $"Completed — {entry.Status}";
|
||||
CounterText.Text = entry.Context;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(ex.Message, "DB → Device", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<UserControl x:Class="HikvisionAttendanceManager.App.DbToDeviceView"
|
||||
<UserControl x:Class="HikvisionAttendanceManager.App.DeviceToDeviceView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<UserControl.Resources>
|
||||
|
|
@ -171,8 +171,8 @@
|
|||
<StackPanel Margin="0,2,0,0">
|
||||
<DockPanel LastChildFill="False">
|
||||
<StackPanel DockPanel.Dock="Left">
|
||||
<TextBlock Text="DB -> Device" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Text="Transfer users registered to a source machine in DB onto a different target Hikvision device (JPEG face templates only)." Style="{StaticResource PageSubtitle}"/>
|
||||
<TextBlock Text="Device → Device" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Text="Transfer users from one Hikvision device to another." Style="{StaticResource PageSubtitle}"/>
|
||||
</StackPanel>
|
||||
<Button x:Name="RefreshButton" Content="↻ Refresh" Style="{StaticResource HeaderRefreshButton}" DockPanel.Dock="Right" VerticalAlignment="Top" Click="Refresh_Click"/>
|
||||
</DockPanel>
|
||||
|
|
@ -187,20 +187,43 @@
|
|||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Source Machine (DB)" Style="{StaticResource FieldLabel}"/>
|
||||
<TextBlock Text="Source Machine" Style="{StaticResource FieldLabel}"/>
|
||||
<ComboBox x:Name="SourceCombo" Style="{StaticResource ModernComboBox}" SelectionChanged="SourceChanged"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<Ellipse x:Name="SourceStatusDot" Width="8" Height="8" Fill="#9CA3AF" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="SourceStatusText" Margin="8,0,0,0" FontSize="12" Foreground="#64748B" Text="Select a device"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2">
|
||||
<TextBlock Text="Target Machine" Style="{StaticResource FieldLabel}"/>
|
||||
<ComboBox x:Name="TargetCombo" Style="{StaticResource ModernComboBox}"/>
|
||||
<ComboBox x:Name="TargetCombo" Style="{StaticResource ModernComboBox}" SelectionChanged="TargetChanged"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<Ellipse x:Name="TargetStatusDot" Width="8" Height="8" Fill="#9CA3AF" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="TargetStatusText" Margin="8,0,0,0" FontSize="12" Foreground="#64748B" Text="Select a device"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,24,0,0">
|
||||
<Button Style="{StaticResource OutlineButton}" Click="LoadUsers_Click">
|
||||
<Border x:Name="SourceInfoBanner" Visibility="Collapsed" Margin="0,18,0,0" Padding="14,10" CornerRadius="6" BorderThickness="1">
|
||||
<TextBlock x:Name="SourceInfoText" TextWrapping="Wrap" FontSize="12"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="SameDeviceWarning" Visibility="Collapsed" Margin="0,14,0,0" FontSize="12" Foreground="#DC2626"
|
||||
Text="Source and target devices must be different."/>
|
||||
|
||||
<TextBlock x:Name="TargetOfflineWarning" Visibility="Collapsed" Margin="0,10,0,0" FontSize="12" Foreground="#DC2626"
|
||||
Text="Target device is not connected. Connect the target device before transferring users."/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource ModernCard}" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="USERS" Style="{StaticResource SectionLabel}"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,18,0,0">
|
||||
<Button x:Name="LoadUsersButton" Style="{StaticResource OutlineButton}" Click="LoadUsers_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Stroke="#2563EB" StrokeThickness="1.7" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,8,0" Data="M 3 4 C 3 2.9 11 2.9 11 4 V 12 C 11 13.1 3 13.1 3 12 Z M 3 4 C 3 5.1 11 5.1 11 4 M 3 8 C 3 9.1 11 9.1 11 8"/>
|
||||
<TextBlock Text="Load Users from Source DB"/>
|
||||
<TextBlock Text="Load Users"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource NeutralButton}" Margin="16,0,0,0" Click="SelectAll_Click">
|
||||
|
|
@ -216,10 +239,10 @@
|
|||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource ModernCard}" Padding="16" Margin="0,0,0,16">
|
||||
<TextBlock x:Name="LoadStatusText" Margin="0,14,0,0" FontSize="12" Foreground="#475569" TextWrapping="Wrap"/>
|
||||
<TextBlock x:Name="SelectionCountText" Margin="0,8,0,14" FontSize="12" Foreground="#1F2A44" FontWeight="SemiBold" Text="0 user(s) loaded • 0 selected"/>
|
||||
|
||||
<Grid>
|
||||
<DataGrid x:Name="UsersGrid"
|
||||
AutoGenerateColumns="False"
|
||||
|
|
@ -239,7 +262,7 @@
|
|||
ColumnHeaderStyle="{StaticResource UsersGridHeaderStyle}"
|
||||
CellStyle="{StaticResource UsersGridCellStyle}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="110" MinWidth="95" IsReadOnly="False">
|
||||
<DataGridTemplateColumn Width="90" MinWidth="80" IsReadOnly="False">
|
||||
<DataGridTemplateColumn.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Click="SelectAll_Click" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
|
|
@ -254,9 +277,10 @@
|
|||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Header="Serial" Binding="{Binding SerialNumber}" Width="140" MinWidth="110" ElementStyle="{StaticResource UsersTextCell}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding EmployeeName}" Width="*" MinWidth="260" ElementStyle="{StaticResource UsersTextCell}" IsReadOnly="True"/>
|
||||
<DataGridCheckBoxColumn Header="Face in DB" Binding="{Binding HasFaceTemplate, Mode=OneWay}" Width="120" MinWidth="100" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Employee / Serial" Binding="{Binding SerialNumber}" Width="140" MinWidth="110" ElementStyle="{StaticResource UsersTextCell}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding EmployeeName}" Width="*" MinWidth="200" ElementStyle="{StaticResource UsersTextCell}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Source" Binding="{Binding SourceDisplay}" Width="90" MinWidth="70" ElementStyle="{StaticResource UsersTextCell}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Face" Binding="{Binding FaceDisplay}" Width="70" MinWidth="60" ElementStyle="{StaticResource UsersTextCell}" IsReadOnly="True"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
|
|
@ -267,12 +291,13 @@
|
|||
</Grid>
|
||||
</Border>
|
||||
<TextBlock Text="No users loaded." Margin="0,16,0,0" Foreground="#10203D" FontSize="13" FontWeight="SemiBold" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Click "Load Users from Source DB" to fetch users." Margin="0,8,0,0" Foreground="#718096" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Select a source machine, then click "Load Users"." Margin="0,8,0,0" Foreground="#718096" FontSize="12" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Button Style="{StaticResource TransferButton}" Click="Transfer_Click">
|
||||
<Button x:Name="TransferButton" Style="{StaticResource TransferButton}" IsEnabled="False" Click="Transfer_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Stroke="White" StrokeThickness="1.7" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,8,0" Data="M 8 13 V 4 M 4 8 L 8 4 L 12 8 M 3 13 V 16 H 13 V 13"/>
|
||||
<TextBlock Text="Transfer Selected to Target"/>
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using HikvisionAttendanceManager.App.Models;
|
||||
using HikvisionAttendanceManager.App.Services;
|
||||
|
||||
namespace HikvisionAttendanceManager.App;
|
||||
|
||||
public partial class DeviceToDeviceView : UserControl
|
||||
{
|
||||
private readonly DeviceService _devices = new();
|
||||
private readonly DeviceConnectionService _connections = new();
|
||||
private readonly AttendanceMachineUserRepository _machineUsers = new();
|
||||
private readonly AttendanceMachineFaceTemplateRepository _faceTemplates = new();
|
||||
private readonly HikvisionIsapiClient _hikvision = new();
|
||||
private readonly TemplateSyncService _service;
|
||||
private readonly ObservableCollection<SelectableMachineUser> _rows = [];
|
||||
private CancellationTokenSource? _sourceStatusCts;
|
||||
private CancellationTokenSource? _targetStatusCts;
|
||||
private bool? _sourceConnected;
|
||||
private bool? _targetConnected;
|
||||
private bool _initialized;
|
||||
|
||||
public DeviceToDeviceView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_service = new TemplateSyncService(_hikvision, _machineUsers, _faceTemplates, new OperationHistoryService());
|
||||
UsersGrid.ItemsSource = _rows;
|
||||
UpdateUsersEmptyState();
|
||||
UpdateSelectionCount();
|
||||
UpdateActionState();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
await RefreshAsync();
|
||||
}
|
||||
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
var originalContent = RefreshButton.Content;
|
||||
try
|
||||
{
|
||||
RefreshButton.IsEnabled = false;
|
||||
RefreshButton.Content = "Refreshing...";
|
||||
_sourceStatusCts?.Cancel();
|
||||
_targetStatusCts?.Cancel();
|
||||
SourceCombo.SelectedItem = null;
|
||||
TargetCombo.SelectedItem = null;
|
||||
ClearUserList();
|
||||
ProgressCard.Visibility = Visibility.Collapsed;
|
||||
ProgressBar.Value = 0;
|
||||
ProgressText.Text = "";
|
||||
CounterText.Text = "";
|
||||
LoadStatusText.Text = "";
|
||||
SourceInfoBanner.Visibility = Visibility.Collapsed;
|
||||
SameDeviceWarning.Visibility = Visibility.Collapsed;
|
||||
TargetOfflineWarning.Visibility = Visibility.Collapsed;
|
||||
_sourceConnected = null;
|
||||
_targetConnected = null;
|
||||
SetConnectionStatus(SourceStatusDot, SourceStatusText, null);
|
||||
SetConnectionStatus(TargetStatusDot, TargetStatusText, null);
|
||||
var devices = await _devices.LoadUnifiedAsync();
|
||||
SourceCombo.ItemsSource = devices;
|
||||
TargetCombo.ItemsSource = devices;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RefreshButton.IsEnabled = true;
|
||||
RefreshButton.Content = originalContent;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Refresh_Click(object sender, RoutedEventArgs e) => await RefreshAsync();
|
||||
|
||||
private async void SourceChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
ClearUserList();
|
||||
LoadStatusText.Text = "";
|
||||
SourceInfoBanner.Visibility = Visibility.Collapsed;
|
||||
await CheckSourceConnectionAsync();
|
||||
UpdateSourceInfoBanner();
|
||||
UpdateActionState();
|
||||
}
|
||||
|
||||
private async void TargetChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
await CheckTargetConnectionAsync();
|
||||
UpdateActionState();
|
||||
}
|
||||
|
||||
private async Task CheckSourceConnectionAsync()
|
||||
{
|
||||
_sourceStatusCts?.Cancel();
|
||||
_sourceStatusCts = new CancellationTokenSource();
|
||||
var token = _sourceStatusCts.Token;
|
||||
|
||||
if (SourceCombo.SelectedItem is not Device device)
|
||||
{
|
||||
_sourceConnected = null;
|
||||
SetConnectionStatus(SourceStatusDot, SourceStatusText, null);
|
||||
return;
|
||||
}
|
||||
|
||||
SetConnectionStatus(SourceStatusDot, SourceStatusText, null, "Checking connection...");
|
||||
try
|
||||
{
|
||||
var result = await _connections.TestConnectivityAsync(device, token).ConfigureAwait(true);
|
||||
if (token.IsCancellationRequested) return;
|
||||
_sourceConnected = result.IsConnected;
|
||||
SetConnectionStatus(SourceStatusDot, SourceStatusText, result.IsConnected,
|
||||
result.IsConnected ? "Connected" : "Not Connected");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
_sourceConnected = false;
|
||||
SetConnectionStatus(SourceStatusDot, SourceStatusText, false, "Not Connected");
|
||||
AppLogger.Warning($"Device→Device source connection check failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckTargetConnectionAsync()
|
||||
{
|
||||
_targetStatusCts?.Cancel();
|
||||
_targetStatusCts = new CancellationTokenSource();
|
||||
var token = _targetStatusCts.Token;
|
||||
|
||||
if (TargetCombo.SelectedItem is not Device device)
|
||||
{
|
||||
_targetConnected = null;
|
||||
SetConnectionStatus(TargetStatusDot, TargetStatusText, null);
|
||||
TargetOfflineWarning.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
SetConnectionStatus(TargetStatusDot, TargetStatusText, null, "Checking connection...");
|
||||
try
|
||||
{
|
||||
var result = await _connections.TestConnectivityAsync(device, token).ConfigureAwait(true);
|
||||
if (token.IsCancellationRequested) return;
|
||||
_targetConnected = result.IsConnected;
|
||||
SetConnectionStatus(TargetStatusDot, TargetStatusText, result.IsConnected,
|
||||
result.IsConnected ? "Connected" : "Not Connected");
|
||||
TargetOfflineWarning.Visibility = result.IsConnected ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
_targetConnected = false;
|
||||
SetConnectionStatus(TargetStatusDot, TargetStatusText, false, "Not Connected");
|
||||
TargetOfflineWarning.Visibility = Visibility.Visible;
|
||||
AppLogger.Warning($"Device→Device target connection check failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSourceInfoBanner()
|
||||
{
|
||||
if (SourceCombo.SelectedItem is null || _sourceConnected is null)
|
||||
{
|
||||
SourceInfoBanner.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
SourceInfoBanner.Visibility = Visibility.Visible;
|
||||
if (_sourceConnected == true)
|
||||
{
|
||||
SourceInfoBanner.Background = new SolidColorBrush(Color.FromRgb(236, 253, 245));
|
||||
SourceInfoBanner.BorderBrush = new SolidColorBrush(Color.FromRgb(167, 243, 208));
|
||||
SourceInfoText.Foreground = new SolidColorBrush(Color.FromRgb(4, 120, 87));
|
||||
SourceInfoText.Text = "Source device connected. Users will be loaded directly from the device.";
|
||||
}
|
||||
else
|
||||
{
|
||||
SourceInfoBanner.Background = new SolidColorBrush(Color.FromRgb(255, 251, 235));
|
||||
SourceInfoBanner.BorderBrush = new SolidColorBrush(Color.FromRgb(253, 230, 138));
|
||||
SourceInfoText.Foreground = new SolidColorBrush(Color.FromRgb(180, 83, 9));
|
||||
SourceInfoText.Text = "Source device is offline. Users will be loaded from the database for the selected source device.";
|
||||
}
|
||||
}
|
||||
|
||||
private async void LoadUsers_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SourceCombo.SelectedItem is not Device source)
|
||||
{
|
||||
MessageBox.Show("Select a source machine.", "Device → Device", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sourceConnected is null)
|
||||
await CheckSourceConnectionAsync();
|
||||
|
||||
UpdateSourceInfoBanner();
|
||||
_rows.Clear();
|
||||
LoadStatusText.Text = "Loading users...";
|
||||
LoadUsersButton.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (_sourceConnected == true)
|
||||
{
|
||||
var users = await _hikvision.GetUsersAsync(source, CancellationToken.None);
|
||||
foreach (var user in users.OrderBy(u => u.EmployeeNo, StringComparer.OrdinalIgnoreCase))
|
||||
AddRow(user.EmployeeNo, user.Name, user.FaceCount > 0, "Device");
|
||||
|
||||
LoadStatusText.Text = $"Loaded {users.Count} user(s) from source Hikvision device.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var users = await _machineUsers.GetActiveByMachineAsync(source.MachineId, CancellationToken.None);
|
||||
foreach (var user in users.OrderBy(u => u.SerialNumber, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var template = await _faceTemplates.TryGetActiveAsync(user.SerialNumber, CancellationToken.None);
|
||||
var hasFace = template?.Template is { Length: > 0 } &&
|
||||
HikvisionIsapiClient.IsUploadableFaceTemplate(template.Template);
|
||||
AddRow(user.SerialNumber, user.EmployeeName, hasFace, "DB");
|
||||
}
|
||||
|
||||
LoadStatusText.Text = $"Source device offline — loaded {users.Count} user(s) from DB.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoadStatusText.Text = "Unable to load users: " + ex.Message;
|
||||
MessageBox.Show(ex.Message, "Device → Device", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadUsersButton.IsEnabled = true;
|
||||
UpdateUsersEmptyState();
|
||||
UpdateSelectionCount();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRow(string serialNumber, string employeeName, bool hasFace, string sourceDisplay)
|
||||
{
|
||||
var row = new SelectableMachineUser(serialNumber, employeeName, hasFace, sourceDisplay);
|
||||
row.PropertyChanged += RowSelectionChanged;
|
||||
_rows.Add(row);
|
||||
}
|
||||
|
||||
private void RowSelectionChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(SelectableMachineUser.IsSelected))
|
||||
{
|
||||
UpdateSelectionCount();
|
||||
UpdateActionState();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearUserList()
|
||||
{
|
||||
foreach (var row in _rows)
|
||||
row.PropertyChanged -= RowSelectionChanged;
|
||||
_rows.Clear();
|
||||
UpdateUsersEmptyState();
|
||||
UpdateSelectionCount();
|
||||
}
|
||||
|
||||
private void SelectAll_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
foreach (var row in _rows) row.IsSelected = true;
|
||||
UsersGrid.Items.Refresh();
|
||||
UpdateSelectionCount();
|
||||
UpdateActionState();
|
||||
}
|
||||
|
||||
private void ClearSelection_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
foreach (var row in _rows) row.IsSelected = false;
|
||||
UsersGrid.Items.Refresh();
|
||||
UpdateSelectionCount();
|
||||
UpdateActionState();
|
||||
}
|
||||
|
||||
private void UpdateUsersEmptyState()
|
||||
{
|
||||
UsersEmptyState.Visibility = _rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void UpdateSelectionCount()
|
||||
{
|
||||
var selected = _rows.Count(r => r.IsSelected);
|
||||
SelectionCountText.Text = $"{_rows.Count} user(s) loaded • {selected} selected";
|
||||
}
|
||||
|
||||
private bool IsSameDeviceSelected()
|
||||
{
|
||||
if (SourceCombo.SelectedItem is not Device source || TargetCombo.SelectedItem is not Device target)
|
||||
return false;
|
||||
|
||||
return string.Equals(source.MachineId, target.MachineId, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(source.IpAddress, target.IpAddress, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void UpdateActionState()
|
||||
{
|
||||
var sameDevice = IsSameDeviceSelected();
|
||||
SameDeviceWarning.Visibility = sameDevice ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
var canTransfer = !sameDevice &&
|
||||
_targetConnected == true &&
|
||||
_rows.Any(r => r.IsSelected) &&
|
||||
SourceCombo.SelectedItem is Device &&
|
||||
TargetCombo.SelectedItem is Device;
|
||||
|
||||
TransferButton.IsEnabled = canTransfer;
|
||||
}
|
||||
|
||||
private async void Transfer_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SourceCombo.SelectedItem is not Device source || TargetCombo.SelectedItem is not Device target)
|
||||
{
|
||||
MessageBox.Show("Select source and target machines.", "Device → Device", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsSameDeviceSelected())
|
||||
{
|
||||
MessageBox.Show("Source and target devices must be different.", "Device → Device", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_targetConnected is null)
|
||||
await CheckTargetConnectionAsync();
|
||||
|
||||
if (_targetConnected != true)
|
||||
{
|
||||
MessageBox.Show("Target device is not connected. Connect the target device before transferring users.",
|
||||
"Device → Device", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sourceConnected is null)
|
||||
await CheckSourceConnectionAsync();
|
||||
|
||||
var selected = _rows.Where(r => r.IsSelected)
|
||||
.Select(r => new DeviceTransferUser(r.SerialNumber, r.EmployeeName))
|
||||
.ToList();
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
MessageBox.Show("Select at least one user.", "Device → Device", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
ProgressCard.Visibility = Visibility.Visible;
|
||||
TransferButton.IsEnabled = false;
|
||||
var progress = new Progress<UserSyncProgress>(p =>
|
||||
{
|
||||
ProgressBar.Maximum = Math.Max(1, p.Total);
|
||||
ProgressBar.Value = p.Completed;
|
||||
ProgressText.Text = $"{p.Completed}/{p.Total} · {p.CurrentEmployee} · {p.CurrentOperation}";
|
||||
CounterText.Text = $"Users: {p.Created} · Faces: {p.FacesUploaded} · Skipped: {p.Skipped} · Failed: {p.Failed}";
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var entry = await _service.DeviceToDeviceAsync(source, target, selected, _sourceConnected == true, progress, CancellationToken.None);
|
||||
ProgressText.Text = $"Completed — {entry.Status}";
|
||||
CounterText.Text = entry.Context;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(ex.Message, "Device → Device", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UpdateActionState();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetConnectionStatus(System.Windows.Shapes.Ellipse dot, TextBlock label, bool? connected, string? text = null)
|
||||
{
|
||||
if (connected is null)
|
||||
{
|
||||
dot.Fill = new SolidColorBrush(Color.FromRgb(156, 163, 175));
|
||||
label.Text = text ?? "Select a device";
|
||||
label.Foreground = new SolidColorBrush(Color.FromRgb(100, 116, 139));
|
||||
return;
|
||||
}
|
||||
|
||||
if (connected == true)
|
||||
{
|
||||
dot.Fill = new SolidColorBrush(Color.FromRgb(22, 163, 74));
|
||||
label.Text = text ?? "Connected";
|
||||
label.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74));
|
||||
}
|
||||
else
|
||||
{
|
||||
dot.Fill = new SolidColorBrush(Color.FromRgb(220, 38, 38));
|
||||
label.Text = text ?? "Not Connected";
|
||||
label.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue