feat(ui): implement DB→Device template sync view

DB→Device template sync UI wired to TemplateSyncService.
main
SYED MUSTUFA AHMED NAQVI 2026-08-27 11:12:59 +05:00
parent 059b667901
commit 425cb4954e
2 changed files with 312 additions and 34 deletions

View File

@ -1,50 +1,289 @@
<UserControl x:Class="HikvisionAttendanceManager.App.DbToDeviceView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<Style x:Key="PageTitle" TargetType="TextBlock">
<Setter Property="FontSize" Value="26"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#10203D"/>
</Style>
<Style x:Key="PageSubtitle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#52627A"/>
<Setter Property="Margin" Value="0,10,0,26"/>
</Style>
<Style x:Key="ModernCard" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#DDE5F0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="Padding" Value="22"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="18" ShadowDepth="3" Opacity="0.07" Color="#4A5D78"/>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SectionLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#65748C"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="FieldLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,7"/>
</Style>
<Style x:Key="ModernComboBox" TargetType="ComboBox">
<Setter Property="Height" Value="36"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#334155"/>
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#CBD7E6"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="ItemTemplate" Value="{StaticResource DeviceComboItemTemplate}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<ToggleButton Background="White"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Focusable="False"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="5">
<Path Width="10"
Height="6"
Stretch="Uniform"
Fill="#526A8A"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,16,0"
Data="M 0 0 L 5 5 L 10 0 Z"/>
</Border>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ContentPresenter Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding ItemTemplate}"
IsHitTestVisible="False"
Margin="14,0,44,0"
VerticalAlignment="Center"
HorizontalAlignment="Left"/>
<Popup Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Slide">
<Border Background="White"
BorderBrush="#CBD7E6"
BorderThickness="1"
CornerRadius="5"
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}">
<ScrollViewer MaxHeight="260">
<ItemsPresenter/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="BorderBrush" Value="#7AA7F8"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="OutlineButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Height" Value="40"/>
<Setter Property="Padding" Value="16,0"/>
<Setter Property="Foreground" Value="#2563EB"/>
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#C9D7EA"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style x:Key="NeutralButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Height" Value="40"/>
<Setter Property="Padding" Value="16,0"/>
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#D7E0EC"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style x:Key="TransferButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Height" Value="38"/>
<Setter Property="Padding" Value="18,0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<Style x:Key="UsersGridHeaderStyle" TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#FBFCFE"/>
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Height" Value="42"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="BorderBrush" Value="#DDE5F0"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<Style x:Key="UsersGridCellStyle" TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="Foreground" Value="#10203D"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#EFF6FF"/>
<Setter Property="Foreground" Value="#1D4ED8"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="UsersTextCell" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="ToolTip" Value="{Binding Text, RelativeSource={RelativeSource Self}}"/>
</Style>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Text="DB → Device" FontSize="25" FontWeight="SemiBold" Foreground="#17233B"/>
<TextBlock Text="Transfer users registered to a source machine in DB onto a different target Hikvision device (JPEG face templates only)." Margin="0,6,0,22" Foreground="#66758C"/>
<Border Style="{StaticResource CardStyle}" Margin="0,0,0,14">
<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}"/>
</StackPanel>
<Button x:Name="RefreshButton" Content="↻ Refresh" Style="{StaticResource HeaderRefreshButton}" DockPanel.Dock="Right" VerticalAlignment="Top" Click="Refresh_Click"/>
</DockPanel>
<Border Style="{StaticResource ModernCard}" Margin="0,0,0,20">
<StackPanel>
<TextBlock Text="MACHINES" Style="{StaticResource CardCaption}"/>
<Grid Margin="0,12,0,0">
<TextBlock Text="MACHINES" Style="{StaticResource SectionLabel}"/>
<Grid Margin="0,20,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,10,0">
<TextBlock Text="Source Machine (DB)" Foreground="#475569"/>
<ComboBox x:Name="SourceCombo" DisplayMemberPath="Name" SelectionChanged="SourceChanged"/>
<StackPanel Grid.Column="0">
<TextBlock Text="Source Machine (DB)" Style="{StaticResource FieldLabel}"/>
<ComboBox x:Name="SourceCombo" Style="{StaticResource ModernComboBox}" SelectionChanged="SourceChanged"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Target Machine" Foreground="#475569"/>
<ComboBox x:Name="TargetCombo" DisplayMemberPath="Name"/>
<StackPanel Grid.Column="2">
<TextBlock Text="Target Machine" Style="{StaticResource FieldLabel}"/>
<ComboBox x:Name="TargetCombo" Style="{StaticResource ModernComboBox}"/>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,14,0,0">
<Button Content="Load Users from Source DB" Click="LoadUsers_Click"/>
<Button Content="Select All" Margin="8,0,0,0" Click="SelectAll_Click"/>
<Button Content="Clear Selection" Margin="8,0,0,0" Click="ClearSelection_Click"/>
<StackPanel Orientation="Horizontal" Margin="0,24,0,0">
<Button 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"/>
</StackPanel>
</Button>
<Button Style="{StaticResource NeutralButton}" Margin="16,0,0,0" Click="SelectAll_Click">
<StackPanel Orientation="Horizontal">
<Path Width="15" Height="15" Stretch="Uniform" Stroke="#526A8A" StrokeThickness="1.6" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,8,0" Data="M 2 2 H 13 V 13 H 2 Z M 4.5 7.5 L 6.5 9.5 L 10.5 5.5"/>
<TextBlock Text="Select All"/>
</StackPanel>
</Button>
<Button Style="{StaticResource NeutralButton}" Margin="16,0,0,0" Click="ClearSelection_Click">
<StackPanel Orientation="Horizontal">
<Path Width="15" Height="15" Stretch="Uniform" Stroke="#526A8A" StrokeThickness="1.6" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,8,0" Data="M 2 4 H 13 M 5 4 V 2 H 10 V 4 M 4 4 L 4.5 13 H 10.5 L 11 4"/>
<TextBlock Text="Clear Selection"/>
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}" Margin="0,0,0,14">
<DataGrid x:Name="UsersGrid" AutoGenerateColumns="False" CanUserAddRows="False" MaxHeight="280">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="Select" Binding="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" Width="70"/>
<DataGridTextColumn Header="Serial" Binding="{Binding SerialNumber}" Width="100"/>
<DataGridTextColumn Header="Name" Binding="{Binding EmployeeName}" Width="*"/>
<DataGridCheckBoxColumn Header="Face in DB" Binding="{Binding HasFaceTemplate, Mode=OneWay}" Width="90" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
<Border Style="{StaticResource ModernCard}" Padding="16" Margin="0,0,0,16">
<Grid>
<DataGrid x:Name="UsersGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
IsReadOnly="False"
HeadersVisibility="Column"
RowHeight="42"
MinHeight="220"
MaxHeight="280"
BorderBrush="#DDE5F0"
BorderThickness="1"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#EEF2F7"
Background="White"
RowBackground="White"
AlternatingRowBackground="White"
ColumnHeaderStyle="{StaticResource UsersGridHeaderStyle}"
CellStyle="{StaticResource UsersGridCellStyle}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="110" MinWidth="95" IsReadOnly="False">
<DataGridTemplateColumn.Header>
<StackPanel Orientation="Horizontal">
<CheckBox Click="SelectAll_Click" VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBlock Text="Select"/>
</StackPanel>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</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"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel x:Name="UsersEmptyState" HorizontalAlignment="Center" VerticalAlignment="Center" IsHitTestVisible="False">
<Border Width="68" Height="68" CornerRadius="7" Background="#EEF4FB" HorizontalAlignment="Center">
<Grid>
<Path Width="35" Height="35" Stretch="Uniform" Stroke="#A7BCE0" StrokeThickness="1.9" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Fill="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center" Data="M 11 13 A 5 5 0 1 1 11 3 A 5 5 0 0 1 11 13 M 3 24 C 4.2 17 17.8 17 19 24 M 1 29 H 20 M 1 34 H 13 M 17 33 H 19"/>
</Grid>
</Border>
<TextBlock Text="No users loaded." Margin="0,16,0,0" Foreground="#10203D" FontSize="13" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock Text="Click &quot;Load Users from Source DB&quot; to fetch users." Margin="0,8,0,0" Foreground="#718096" FontSize="12" HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</Border>
<Button Content="Transfer Selected to Target" Style="{StaticResource PrimaryButton}" HorizontalAlignment="Left" Click="Transfer_Click"/>
<Border x:Name="ProgressCard" Style="{StaticResource CardStyle}" Visibility="Collapsed" Margin="0,14,0,0">
<Button Style="{StaticResource TransferButton}" 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"/>
</StackPanel>
</Button>
<Border x:Name="ProgressCard" Style="{StaticResource ModernCard}" Visibility="Collapsed" Margin="0,16,0,0">
<StackPanel>
<ProgressBar x:Name="ProgressBar" Height="12" Margin="0,0,0,8"/>
<TextBlock x:Name="ProgressText" Foreground="#475569"/>
<TextBlock x:Name="CounterText" Margin="0,8,0,0" Foreground="#475569"/>
<ProgressBar x:Name="ProgressBar" Height="10" Margin="0,0,0,12"/>
<TextBlock x:Name="ProgressText" Foreground="#475569" FontSize="13" TextWrapping="Wrap"/>
<TextBlock x:Name="CounterText" Margin="0,8,0,0" Foreground="#475569" FontSize="13"/>
</StackPanel>
</Border>
</StackPanel>

View File

@ -20,18 +20,50 @@ public partial class DbToDeviceView : UserControl
InitializeComponent();
_service = new TemplateSyncService(new HikvisionIsapiClient(), _machineUsers, _faceTemplates, new OperationHistoryService());
UsersGrid.ItemsSource = _rows;
UpdateUsersEmptyState();
}
public async Task InitializeAsync()
{
if (_initialized) return;
_initialized = true;
var devices = await _devices.LoadUnifiedAsync();
SourceCombo.ItemsSource = devices;
TargetCombo.ItemsSource = devices;
await RefreshAsync();
}
private void SourceChanged(object sender, SelectionChangedEventArgs e) => _rows.Clear();
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)
{
@ -44,6 +76,8 @@ public partial class DbToDeviceView : UserControl
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)
@ -58,6 +92,11 @@ public partial class DbToDeviceView : UserControl
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)