feat(ui): implement Departmental User Sync and Sync History views

Departmental user sync and sync history pages, completing the workflow UI set
main
SYED MUSTUFA AHMED NAQVI 2026-08-27 11:13:21 +05:00
parent 425cb4954e
commit 202d490980
4 changed files with 977 additions and 80 deletions

View File

@ -1,81 +1,502 @@
<UserControl x:Class="HikvisionAttendanceManager.App.DepartmentalUserSyncView"
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="25"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#10203D"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="PageSubtitle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#52627A"/>
<Setter Property="Margin" Value="0,8,0,24"/>
</Style>
<Style x:Key="SyncCard" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#DDE5F0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="22"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="18" ShadowDepth="2" Opacity="0.07" Color="#4A5D78"/>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SectionLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#65748C"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="FieldLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontSize" Value="11"/>
<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="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="4">
<Path Width="9"
Height="6"
Stretch="Uniform"
Fill="#526A8A"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,14,0"
Data="M 0 0 L 5 5 L 10 0 Z"/>
</Border>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ContentPresenter x:Name="ContentSite"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding ItemTemplate}"
IsHitTestVisible="False"
Margin="12,0,42,0"
VerticalAlignment="Center"
HorizontalAlignment="Left"/>
<TextBox x:Name="PART_EditableTextBox"
Background="Transparent"
BorderThickness="0"
Foreground="{TemplateBinding Foreground}"
FontSize="{TemplateBinding FontSize}"
Padding="0"
Margin="12,0,42,0"
VerticalContentAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed"/>
<TextBlock x:Name="PlaceholderText"
Text="{TemplateBinding Tag}"
Foreground="#78869A"
FontSize="{TemplateBinding FontSize}"
Margin="12,0,42,0"
VerticalAlignment="Center"
IsHitTestVisible="False"
Visibility="Collapsed"/>
<Popup Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Slide">
<Border Background="White"
BorderBrush="#CBD7E6"
BorderThickness="1"
CornerRadius="4"
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}">
<ScrollViewer MaxHeight="260">
<ItemsPresenter/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="SelectedIndex" Value="-1">
<Setter TargetName="ContentSite" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="PlaceholderText" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsEditable" Value="True">
<Setter TargetName="ContentSite" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="PART_EditableTextBox" Property="Visibility" Value="Visible"/>
<Setter TargetName="PlaceholderText" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="BorderBrush" Value="#7AA7F8"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ModernSearchTextBox" TargetType="TextBox">
<Setter Property="Height" Value="36"/>
<Setter Property="Padding" Value="6,0"/>
<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="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DeviceComboBox" TargetType="ComboBox" BasedOn="{StaticResource ModernComboBox}">
<Setter Property="ItemTemplate" Value="{StaticResource DeviceComboItemTemplate}"/>
<Setter Property="Tag" Value="Select target device..."/>
</Style>
<Style x:Key="SiteComboBox" TargetType="ComboBox" BasedOn="{StaticResource ModernComboBox}">
<Setter Property="ItemTemplate" Value="{StaticResource LocationSiteComboItemTemplate}"/>
<Setter Property="TextSearch.TextPath" Value="DisplayName"/>
<Setter Property="Tag" Value="Select location site..."/>
<Setter Property="IsEditable" Value="True"/>
<Setter Property="IsTextSearchEnabled" Value="False"/>
<Setter Property="StaysOpenOnEdit" Value="True"/>
</Style>
<Style x:Key="RadioChoice" TargetType="RadioButton">
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="0,0,30,0"/>
</Style>
<Style x:Key="OutlineButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Height" Value="34"/>
<Setter Property="Padding" Value="14,0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#DDE5F0"/>
</Style>
<Style x:Key="PrimaryActionButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Height" Value="38"/>
<Setter Property="Padding" Value="16,0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<Style x:Key="EmployeeGridHeaderStyle" TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#FBFCFE"/>
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Height" Value="34"/>
<Setter Property="Padding" Value="10,0"/>
<Setter Property="BorderBrush" Value="#E5EAF2"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<Style x:Key="EmployeeGridCellStyle" TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,0"/>
<Setter Property="Foreground" Value="#10203D"/>
<Setter Property="FontSize" Value="11"/>
<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="EmployeeTextCell" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="ToolTip" Value="{Binding Text, RelativeSource={RelativeSource Self}}"/>
</Style>
<Style x:Key="DepartmentRowCheckBox" TargetType="CheckBox">
<Setter Property="Height" Value="34"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="BorderBrush" Value="#E5EAF2"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="White">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="34"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="Box"
Width="13"
Height="13"
BorderBrush="#8794A8"
BorderThickness="1"
Background="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Path x:Name="CheckMark"
Width="9"
Height="7"
Stretch="Uniform"
Stroke="#2563EB"
StrokeThickness="1.8"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Fill="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed"
Data="M 1 3.5 L 3.5 6 L 8 1"/>
<ContentPresenter Grid.Column="1" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Box" Property="BorderBrush" Value="#2563EB"/>
<Setter TargetName="Box" Property="Background" Value="#EFF6FF"/>
<Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Box" Property="BorderBrush" Value="#2563EB"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Text="Departmental User Sync" FontSize="25" FontWeight="SemiBold" Foreground="#17233B"/>
<TextBlock Text="Bulk create and enroll active employees from selected departments onto a target Hikvision device." Margin="0,6,0,22" Foreground="#66758C"/>
<Border Style="{StaticResource CardStyle}" Margin="0,0,0,14">
<StackPanel Margin="0,0,0,0">
<DockPanel LastChildFill="False">
<StackPanel DockPanel.Dock="Left">
<StackPanel Orientation="Horizontal" Margin="0,0,0,0">
<Path Width="22"
Height="22"
Stretch="Uniform"
Stroke="#536179"
StrokeThickness="1.7"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Fill="Transparent"
Margin="0,0,10,0"
Data="M 3 20 H 19 M 5 20 V 4 H 15 V 20 M 15 9 H 19 V 20 M 8 8 H 9 M 11 8 H 12 M 8 12 H 9 M 11 12 H 12 M 8 16 H 9 M 11 16 H 12"/>
<TextBlock Text="Departmental User Sync" Style="{StaticResource PageTitle}"/>
</StackPanel>
<TextBlock Text="Bulk create and enroll active employees from selected departments onto a target Hikvision device." 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 SyncCard}" Margin="0,0,0,14">
<StackPanel>
<TextBlock Text="TARGET &amp; FILTERS" Style="{StaticResource CardCaption}"/>
<Grid Margin="0,12,0,0">
<TextBlock Text="TARGET &amp; FILTERS" Style="{StaticResource SectionLabel}"/>
<Grid Margin="0,18,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="330"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,10,0">
<TextBlock Text="Target Hikvision Device" Foreground="#475569"/>
<ComboBox x:Name="DeviceCombo" DisplayMemberPath="Name"/>
<StackPanel Grid.Column="0">
<TextBlock Text="Target Hikvision Device" Style="{StaticResource FieldLabel}"/>
<ComboBox x:Name="DeviceCombo" Style="{StaticResource DeviceComboBox}"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Location Site" Foreground="#475569"/>
<ComboBox x:Name="SiteCombo" DisplayMemberPath="DisplayName" SelectionChanged="SiteChanged"/>
<StackPanel Grid.Column="2">
<TextBlock Text="Location Site" Style="{StaticResource FieldLabel}"/>
<ComboBox x:Name="SiteCombo"
Style="{StaticResource SiteComboBox}"
SelectionChanged="SiteChanged"
PreviewKeyUp="SiteCombo_PreviewKeyUp"
DropDownOpened="SiteCombo_DropDownOpened"/>
</StackPanel>
</Grid>
<TextBlock Text="Department" Margin="0,16,0,8" Foreground="#475569" FontWeight="SemiBold"/>
<TextBlock Text="Department" Style="{StaticResource FieldLabel}" Margin="0,18,0,8"/>
<StackPanel Orientation="Horizontal">
<RadioButton x:Name="SelectedDepartmentsMode" Content="Selected Departments" Checked="ModeChanged" Margin="0,0,20,0"/>
<RadioButton x:Name="AllDepartmentsMode" Content="All Active Departments" Checked="ModeChanged"/>
<RadioButton x:Name="SelectedDepartmentsMode" Content="Selected Departments" Checked="ModeChanged" Style="{StaticResource RadioChoice}"/>
<RadioButton x:Name="AllDepartmentsMode" Content="All Active Departments" Checked="ModeChanged" Style="{StaticResource RadioChoice}"/>
</StackPanel>
<Border x:Name="DepartmentSelectorPanel" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,12,0,0" Background="#FAFBFD">
<StackPanel>
<TextBlock Text="Select one or more departments" Foreground="#66758C" FontSize="12" Margin="0,0,0,8"/>
<ItemsControl x:Name="DepartmentItems">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,4,0,0"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock x:Name="DepartmentStatusText" Margin="0,10,0,0" Foreground="#66758C" TextWrapping="Wrap"/>
<StackPanel x:Name="DepartmentSelectorPanel" Margin="0,14,0,0">
<Grid>
<TextBox x:Name="DepartmentSearchBox"
Style="{StaticResource ModernSearchTextBox}"
TextChanged="DepartmentSearchBox_TextChanged"/>
<Path Width="14"
Height="14"
Stretch="Uniform"
Stroke="#64748B"
StrokeThickness="1.8"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Fill="Transparent"
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsHitTestVisible="False"
Visibility="Collapsed"
Data="M 6 11 A 5 5 0 1 1 6 1 A 5 5 0 0 1 6 11 M 10 10 L 14 14"/>
<TextBlock x:Name="DepartmentSearchPlaceholder"
Text="Search departments..."
Foreground="#78869A"
FontSize="12"
Margin="6,0,12,0"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
</Grid>
<Border BorderBrush="#DDE5F0"
BorderThickness="1"
CornerRadius="4"
Background="White"
Margin="0,10,0,0"
Height="320">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="DepartmentItems">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}"
IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource DepartmentRowCheckBox}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</StackPanel>
<TextBlock x:Name="DepartmentStatusText" Margin="0,8,0,0" Foreground="#66758C" FontSize="12" TextWrapping="Wrap"/>
<Button Style="{StaticResource PrimaryActionButton}" Margin="0,18,0,0" Click="LoadEmployees_Click">
<StackPanel Orientation="Horizontal">
<Path Width="14"
Height="14"
Stretch="Uniform"
Stroke="White"
StrokeThickness="1.7"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Fill="Transparent"
Margin="0,0,8,0"
Data="M 6 7 A 3 3 0 1 1 6 1 A 3 3 0 0 1 6 7 M 1 15 C 1.8 11 10.2 11 11 15 M 14 5 V 11 M 11 8 H 17"/>
<TextBlock Text="Load Employees"/>
</StackPanel>
</Button>
</StackPanel>
</Border>
<Border Style="{StaticResource SyncCard}" Padding="22,18" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="EMPLOYEES" Style="{StaticResource SectionLabel}"/>
<StackPanel Orientation="Horizontal" Margin="0,12,0,14">
<Button Style="{StaticResource OutlineButton}" Click="SelectAll_Click">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Stretch="Uniform" Stroke="#64748B" StrokeThickness="1.6" Fill="Transparent" Margin="0,0,8,0" Data="M 2 2 H 12 V 12 H 2 Z M 4 7 L 6.3 9.2 L 10 4.5"/>
<TextBlock Text="Select All"/>
</StackPanel>
</Button>
<Button Style="{StaticResource OutlineButton}" Margin="12,0,0,0" Click="ClearSelection_Click">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Stretch="Uniform" Stroke="#64748B" StrokeThickness="1.6" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Fill="Transparent" Margin="0,0,8,0" Data="M 2 4 H 12 M 5 4 V 2 H 9 V 4 M 4 4 L 4.5 13 H 9.5 L 10 4"/>
<TextBlock Text="Clear Selection"/>
</StackPanel>
</Button>
</StackPanel>
<TextBlock x:Name="EmployeeStatusText" Foreground="#66758C" Margin="0,0,0,8" TextWrapping="Wrap" FontSize="12"/>
<Border BorderBrush="#DDE5F0" BorderThickness="1" Background="White">
<Grid MinHeight="165">
<DataGrid x:Name="EmployeesGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
IsReadOnly="False"
HeadersVisibility="Column"
RowHeight="36"
BorderThickness="0"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#EEF2F7"
Background="White"
RowBackground="White"
AlternatingRowBackground="White"
ColumnHeaderStyle="{StaticResource EmployeeGridHeaderStyle}"
CellStyle="{StaticResource EmployeeGridCellStyle}">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="Select" Binding="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" Width="90"/>
<DataGridTextColumn Header="Serial" Binding="{Binding SerialNumber, Mode=OneWay}" Width="120" IsReadOnly="True" ElementStyle="{StaticResource EmployeeTextCell}"/>
<DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=OneWay}" Width="2*" MinWidth="180" IsReadOnly="True" ElementStyle="{StaticResource EmployeeTextCell}"/>
<DataGridTextColumn Header="Department" Binding="{Binding Department, Mode=OneWay}" Width="2*" MinWidth="180" IsReadOnly="True" ElementStyle="{StaticResource EmployeeTextCell}"/>
<DataGridTextColumn Header="Site" Binding="{Binding LocationSiteDisplay, Mode=OneWay}" Width="1.4*" MinWidth="120" IsReadOnly="True" ElementStyle="{StaticResource EmployeeTextCell}"/>
<DataGridTextColumn Header="Active" Binding="{Binding ActiveDisplay, Mode=OneWay}" Width="90" IsReadOnly="True" ElementStyle="{StaticResource EmployeeTextCell}"/>
<DataGridTextColumn Header="Existing" Binding="{Binding ExistingOnDeviceDisplay, Mode=OneWay}" Width="100" IsReadOnly="True" ElementStyle="{StaticResource EmployeeTextCell}"/>
<DataGridTextColumn Header="Photo" Binding="{Binding PhotoStatus, Mode=OneWay}" Width="130" IsReadOnly="True" ElementStyle="{StaticResource EmployeeTextCell}"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel x:Name="EmployeesEmptyState" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,28,0,0">
<Border Width="54" Height="54" CornerRadius="6" Background="#EEF4FB" HorizontalAlignment="Center">
<Path Width="28"
Height="28"
Stretch="Uniform"
Stroke="#A7BCE0"
StrokeThickness="1.7"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Fill="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 8 8 A 4 4 0 1 1 8 0 A 4 4 0 0 1 8 8 M 1 19 C 2 13 14 13 15 19 M 0 22 H 16"/>
</Border>
<TextBlock Text="No employees loaded." Foreground="#1F2A44" FontWeight="SemiBold" FontSize="12" HorizontalAlignment="Center" Margin="0,14,0,0"/>
<TextBlock Text="Load employees using the filters above." Foreground="#66758C" FontSize="11" HorizontalAlignment="Center" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Border>
<Button Content="Load Employees" Style="{StaticResource PrimaryButton}" HorizontalAlignment="Left" Margin="0,14,0,0" Click="LoadEmployees_Click"/>
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}" Margin="0,0,0,14">
<Button Style="{StaticResource PrimaryActionButton}" Click="StartSync_Click">
<StackPanel Orientation="Horizontal">
<Path Width="14"
Height="14"
Stretch="Uniform"
Stroke="White"
StrokeThickness="1.7"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Fill="Transparent"
Margin="0,0,8,0"
Data="M 7 2 V 11 M 3 7 L 7 11 L 11 7 M 2 13 V 16 H 12 V 13"/>
<TextBlock Text="Create &amp; Enroll Selected on Device"/>
</StackPanel>
</Button>
<Border x:Name="ProgressCard" Style="{StaticResource SyncCard}" Visibility="Collapsed" Margin="0,14,0,0">
<StackPanel>
<DockPanel Margin="0,0,0,12">
<TextBlock Text="EMPLOYEES" Style="{StaticResource CardCaption}" DockPanel.Dock="Left"/>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Content="Select All" Click="SelectAll_Click"/>
<Button Content="Clear Selection" Margin="8,0,0,0" Click="ClearSelection_Click"/>
</StackPanel>
</DockPanel>
<TextBlock x:Name="EmployeeStatusText" Foreground="#66758C" Margin="0,0,0,10" TextWrapping="Wrap"/>
<DataGrid x:Name="EmployeesGrid" AutoGenerateColumns="False" CanUserAddRows="False" MaxHeight="320">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="Select" Binding="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" Width="70"/>
<DataGridTextColumn Header="Serial" Binding="{Binding SerialNumber, Mode=OneWay}" Width="90" IsReadOnly="True"/>
<DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=OneWay}" Width="*"/>
<DataGridTextColumn Header="Department" Binding="{Binding Department, Mode=OneWay}" Width="*"/>
<DataGridTextColumn Header="Site" Binding="{Binding LocationSiteDisplay, Mode=OneWay}" Width="90" IsReadOnly="True"/>
<DataGridTextColumn Header="Active" Binding="{Binding ActiveDisplay, Mode=OneWay}" Width="60" IsReadOnly="True"/>
<DataGridTextColumn Header="Existing" Binding="{Binding ExistingOnDeviceDisplay, Mode=OneWay}" Width="70" IsReadOnly="True"/>
<DataGridTextColumn Header="Photo" Binding="{Binding PhotoStatus, Mode=OneWay}" Width="110" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
<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>
<Button Content="Create &amp; Enroll Selected on Device" Style="{StaticResource PrimaryButton}" HorizontalAlignment="Left" Click="StartSync_Click"/>
<Border x:Name="ProgressCard" Style="{StaticResource CardStyle}" Visibility="Collapsed" Margin="0,14,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"/>
</StackPanel>
</Border>
<Border x:Name="ResultsCard" Style="{StaticResource CardStyle}" Visibility="Collapsed" Margin="0,14,0,0">
<Border x:Name="ResultsCard" Style="{StaticResource SyncCard}" Visibility="Collapsed" Margin="0,14,0,0">
<DataGrid x:Name="ResultsGrid" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False" MaxHeight="220">
<DataGrid.Columns>
<DataGridTextColumn Header="Employee" Binding="{Binding EmployeeNumber}" Width="100"/>

View File

@ -1,6 +1,9 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using HikvisionAttendanceManager.App.Models;
using HikvisionAttendanceManager.App.Services;
@ -14,7 +17,11 @@ public partial class DepartmentalUserSyncView : UserControl
private readonly DepartmentalUserSyncService _service;
private readonly ObservableCollection<SelectableEmployee> _employees = [];
private readonly ObservableCollection<SelectableDepartment> _departments = [];
private readonly ObservableCollection<LocationSite> _sites = [];
private readonly ICollectionView _departmentView;
private readonly ICollectionView _siteView;
private bool _initialized;
private string _siteFilterText = "";
public DepartmentalUserSyncView()
{
@ -23,27 +30,77 @@ public partial class DepartmentalUserSyncView : UserControl
new UserSyncEngine(new HikvisionIsapiClient(), new EmployeePhotoService()),
new OperationHistoryService());
EmployeesGrid.ItemsSource = _employees;
_employees.CollectionChanged += (_, _) => UpdateEmployeeEmptyState();
DepartmentItems.ItemsSource = _departments;
_departmentView = CollectionViewSource.GetDefaultView(_departments);
_departmentView.Filter = FilterDepartment;
_siteView = CollectionViewSource.GetDefaultView(_sites);
_siteView.Filter = FilterSite;
SiteCombo.ItemsSource = _siteView;
SelectedDepartmentsMode.IsChecked = true;
ApplyDepartmentMode();
UpdateEmployeeEmptyState();
}
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...";
ResetPageState();
DeviceCombo.ItemsSource = await _devices.LoadUnifiedAsync();
SiteCombo.ItemsSource = await _hrms.GetLocationSitesAsync(CancellationToken.None);
_sites.Clear();
foreach (var site in await _hrms.GetLocationSitesAsync(CancellationToken.None))
_sites.Add(site);
_siteFilterText = "";
_siteView.Refresh();
}
catch (HrmsDataException ex)
{
DepartmentStatusText.Text = ex.Message;
}
finally
{
RefreshButton.IsEnabled = true;
RefreshButton.Content = originalContent;
}
ApplyDepartmentMode();
}
private async void Refresh_Click(object sender, RoutedEventArgs e) => await RefreshAsync();
private void ResetPageState()
{
DeviceCombo.SelectedItem = null;
SiteCombo.SelectedItem = null;
SiteCombo.Text = "";
_siteFilterText = "";
DepartmentSearchBox.Clear();
_departments.Clear();
_employees.Clear();
DepartmentStatusText.Text = "";
EmployeeStatusText.Text = "";
ProgressCard.Visibility = Visibility.Collapsed;
ResultsCard.Visibility = Visibility.Collapsed;
ResultsGrid.ItemsSource = null;
ProgressBar.Value = 0;
ProgressText.Text = "";
CounterText.Text = "";
SelectedDepartmentsMode.IsChecked = true;
AllDepartmentsMode.IsChecked = false;
_departmentView.Refresh();
_siteView.Refresh();
}
private void ModeChanged(object sender, RoutedEventArgs e) => ApplyDepartmentMode();
private void ApplyDepartmentMode()
@ -53,17 +110,56 @@ public partial class DepartmentalUserSyncView : UserControl
DepartmentSelectorPanel.Visibility = selectedMode ? Visibility.Visible : Visibility.Collapsed;
}
private bool FilterDepartment(object item)
{
if (item is not SelectableDepartment department)
return false;
var query = DepartmentSearchBox?.Text.Trim();
return string.IsNullOrWhiteSpace(query) ||
department.Name.Contains(query, StringComparison.OrdinalIgnoreCase);
}
private bool FilterSite(object item)
{
if (item is not LocationSite site)
return false;
return string.IsNullOrWhiteSpace(_siteFilterText) ||
site.DisplayName.Contains(_siteFilterText, StringComparison.OrdinalIgnoreCase) ||
site.Name.Contains(_siteFilterText, StringComparison.OrdinalIgnoreCase) ||
site.Id.ToString().Contains(_siteFilterText, StringComparison.OrdinalIgnoreCase);
}
private void DepartmentSearchBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (DepartmentSearchPlaceholder is not null)
DepartmentSearchPlaceholder.Visibility = string.IsNullOrWhiteSpace(DepartmentSearchBox.Text)
? Visibility.Visible
: Visibility.Collapsed;
_departmentView.Refresh();
}
private async void SiteChanged(object sender, SelectionChangedEventArgs e)
{
if (SiteCombo.SelectedItem is null && SiteCombo.IsKeyboardFocusWithin && !string.IsNullOrWhiteSpace(SiteCombo.Text))
return;
_employees.Clear();
UpdateEmployeeEmptyState();
EmployeeStatusText.Text = "";
_departments.Clear();
DepartmentSearchBox.Clear();
if (SiteCombo.SelectedItem is not LocationSite site)
{
DepartmentStatusText.Text = "Select a location site to load departments.";
return;
}
_siteFilterText = "";
_siteView.Refresh();
try
{
DepartmentStatusText.Text = "Loading departments…";
@ -80,6 +176,29 @@ public partial class DepartmentalUserSyncView : UserControl
}
}
private void SiteCombo_DropDownOpened(object sender, EventArgs e)
{
if (SiteCombo.SelectedItem is LocationSite selected && SiteCombo.Text == selected.DisplayName)
_siteFilterText = "";
_siteView.Refresh();
}
private void SiteCombo_PreviewKeyUp(object sender, KeyEventArgs e)
{
if (e.Key is Key.Up or Key.Down or Key.Left or Key.Right or Key.Enter or Key.Tab or Key.Escape)
return;
_siteFilterText = SiteCombo.Text.Trim();
_siteView.Refresh();
SiteCombo.IsDropDownOpen = true;
if (SiteCombo.Template.FindName("PART_EditableTextBox", SiteCombo) is TextBox editableTextBox)
{
editableTextBox.CaretIndex = editableTextBox.Text.Length;
editableTextBox.SelectionLength = 0;
}
}
private async void LoadEmployees_Click(object sender, RoutedEventArgs e)
{
if (SiteCombo.SelectedItem is not LocationSite site)
@ -147,6 +266,12 @@ public partial class DepartmentalUserSyncView : UserControl
EmployeesGrid.Items.Refresh();
}
private void UpdateEmployeeEmptyState()
{
if (EmployeesEmptyState is not null)
EmployeesEmptyState.Visibility = _employees.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
private async void StartSync_Click(object sender, RoutedEventArgs e)
{
if (DeviceCombo.SelectedItem is not Device device) { MessageBox.Show("Select a target device."); return; }

View File

@ -1,33 +1,256 @@
<UserControl x:Class="HikvisionAttendanceManager.App.SyncHistoryView"
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"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="PageSubtitle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="#8AA0BE"/>
<Setter Property="Margin" Value="0,8,0,0"/>
</Style>
<Style x:Key="HistoryCard" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#DDE5F0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="18" ShadowDepth="2" Opacity="0.07" Color="#4A5D78"/>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="FilterLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#65748C"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,8"/>
</Style>
<Style x:Key="FilterComboBox" TargetType="ComboBox">
<Setter Property="Height" Value="38"/>
<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="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<Style x:Key="HistoryHeaderStyle" 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="46"/>
<Setter Property="Padding" Value="14,0"/>
<Setter Property="BorderBrush" Value="#E7ECF4"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<Style x:Key="HistoryCellStyle" TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="14,0"/>
<Setter Property="Foreground" Value="#244062"/>
<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="HistoryTextCell" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="ToolTip" Value="{Binding Text, RelativeSource={RelativeSource Self}}"/>
</Style>
<Style x:Key="HistoryContextCell" TargetType="TextBlock" BasedOn="{StaticResource HistoryTextCell}">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="LineHeight" Value="17"/>
<Setter Property="MaxHeight" Value="38"/>
<Setter Property="ClipToBounds" Value="True"/>
</Style>
<Style x:Key="HistoryNumericCell" TargetType="TextBlock" BasedOn="{StaticResource HistoryTextCell}">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="TextAlignment" Value="Center"/>
</Style>
<Style x:Key="HistoryCenteredHeaderStyle" TargetType="DataGridColumnHeader" BasedOn="{StaticResource HistoryHeaderStyle}">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
<Style x:Key="StatusBadge" TargetType="Border">
<Setter Property="Background" Value="#DCFCE7"/>
<Setter Property="CornerRadius" Value="15"/>
<Setter Property="Padding" Value="9,5"/>
<Setter Property="Width" Value="104"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsFailed}" Value="True">
<Setter Property="Background" Value="#FCE1EA"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="StatusText" TargetType="TextBlock">
<Setter Property="Foreground" Value="#168A55"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsFailed}" Value="True">
<Setter Property="Foreground" Value="#CC3B57"/>
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel Margin="0,0,0,16">
<DockPanel LastChildFill="False" Margin="0,0,0,24">
<StackPanel DockPanel.Dock="Left">
<TextBlock Text="Sync History" FontSize="25" FontWeight="SemiBold" Foreground="#17233B"/>
<TextBlock Text="Persistent history for attendance, user, template, and departmental sync operations." Margin="0,6,0,0" Foreground="#66758C"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Path Width="28"
Height="28"
Stretch="Uniform"
Stroke="#17355A"
StrokeThickness="2"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Fill="Transparent"
VerticalAlignment="Center"
Margin="0,0,14,0"
Data="M 14 2 A 12 12 0 1 1 4.8 6.3 M 4 2 V 7 H 9 M 14 7 V 14 L 19 17"/>
<TextBlock Text="Sync History" Style="{StaticResource PageTitle}"/>
</StackPanel>
<TextBlock Text="View the history of attendance, user, template and departmental sync operations." Style="{StaticResource PageSubtitle}"/>
</StackPanel>
<Button DockPanel.Dock="Right" Content="Refresh" VerticalAlignment="Center" Click="Refresh_Click"/>
<Button x:Name="RefreshButton" DockPanel.Dock="Right" Content="↻ Refresh" Style="{StaticResource HeaderRefreshButton}" Click="Refresh_Click"/>
</DockPanel>
<Border Grid.Row="1" Style="{StaticResource CardStyle}" Padding="0">
<DataGrid x:Name="HistoryGrid" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False" BorderThickness="0">
<DataGrid.Columns>
<DataGridTextColumn Header="Date/Time" Binding="{Binding StartedAtDisplay}" Width="160"/>
<DataGridTextColumn Header="Operation" Binding="{Binding OperationDisplay}" Width="150"/>
<DataGridTextColumn Header="Source" Binding="{Binding SourceDevice}" Width="*"/>
<DataGridTextColumn Header="Target" Binding="{Binding TargetDevice}" Width="*"/>
<DataGridTextColumn Header="Context" Binding="{Binding Context}" Width="*"/>
<DataGridTextColumn Header="Total" Binding="{Binding Total}" Width="60"/>
<DataGridTextColumn Header="Success" Binding="{Binding Success}" Width="70"/>
<DataGridTextColumn Header="Failed" Binding="{Binding Failed}" Width="60"/>
<DataGridTextColumn Header="Skipped" Binding="{Binding Skipped}" Width="70"/>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="120"/>
</DataGrid.Columns>
</DataGrid>
<Border Grid.Row="1" Style="{StaticResource HistoryCard}" Padding="20" Margin="0,0,0,14">
<UniformGrid Columns="4" Rows="1">
<StackPanel Margin="0,0,12,0">
<TextBlock Text="Date Range" Style="{StaticResource FilterLabel}"/>
<ComboBox x:Name="DateRangeCombo" Style="{StaticResource FilterComboBox}" SelectionChanged="Filter_Changed"/>
</StackPanel>
<StackPanel Margin="12,0">
<TextBlock Text="Operation" Style="{StaticResource FilterLabel}"/>
<ComboBox x:Name="OperationCombo" Style="{StaticResource FilterComboBox}" SelectionChanged="Filter_Changed"/>
</StackPanel>
<StackPanel Margin="12,0">
<TextBlock Text="Source" Style="{StaticResource FilterLabel}"/>
<ComboBox x:Name="SourceCombo" Style="{StaticResource FilterComboBox}" SelectionChanged="Filter_Changed"/>
</StackPanel>
<StackPanel Margin="12,0,0,0">
<TextBlock Text="Target" Style="{StaticResource FilterLabel}"/>
<ComboBox x:Name="TargetCombo" Style="{StaticResource FilterComboBox}" SelectionChanged="Filter_Changed"/>
</StackPanel>
</UniformGrid>
</Border>
<Border Grid.Row="2" Style="{StaticResource HistoryCard}" Padding="0">
<Grid MinHeight="150">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="34"/>
</Grid.RowDefinitions>
<DataGrid x:Name="HistoryGrid"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
BorderThickness="0"
HeadersVisibility="Column"
RowHeight="54"
MinHeight="100"
MaxHeight="560"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#EEF2F7"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Background="White"
RowBackground="White"
AlternatingRowBackground="White"
ColumnHeaderStyle="{StaticResource HistoryHeaderStyle}"
CellStyle="{StaticResource HistoryCellStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="Date/Time" Binding="{Binding StartedAtText}" Width="145" ElementStyle="{StaticResource HistoryTextCell}"/>
<DataGridTemplateColumn Header="Operation" Width="170">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="30" Height="30" CornerRadius="15" Background="{Binding OperationIconBackground}" Margin="0,0,10,0">
<Path Width="15"
Height="15"
Stretch="Uniform"
Stroke="{Binding OperationIconBrush}"
StrokeThickness="1.7"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Fill="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="{Binding OperationIconData}"/>
</Border>
<TextBlock Text="{Binding OperationText}" Width="118" VerticalAlignment="Center" Foreground="{Binding OperationIconBrush}" TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Source" Binding="{Binding Source}" Width="160" ElementStyle="{StaticResource HistoryTextCell}"/>
<DataGridTextColumn Header="Target" Binding="{Binding Target}" Width="160" ElementStyle="{StaticResource HistoryTextCell}"/>
<DataGridTextColumn Header="Context" Binding="{Binding Context}" Width="*" MinWidth="360" ElementStyle="{StaticResource HistoryContextCell}"/>
<DataGridTextColumn Header="Total" Binding="{Binding Total}" Width="80" ElementStyle="{StaticResource HistoryNumericCell}" HeaderStyle="{StaticResource HistoryCenteredHeaderStyle}"/>
<DataGridTextColumn Header="Success" Binding="{Binding Success}" Width="80" ElementStyle="{StaticResource HistoryNumericCell}" HeaderStyle="{StaticResource HistoryCenteredHeaderStyle}"/>
<DataGridTextColumn Header="Failed" Binding="{Binding Failed}" Width="80" ElementStyle="{StaticResource HistoryNumericCell}" HeaderStyle="{StaticResource HistoryCenteredHeaderStyle}"/>
<DataGridTextColumn Header="Skipped" Binding="{Binding Skipped}" Width="80" ElementStyle="{StaticResource HistoryNumericCell}" HeaderStyle="{StaticResource HistoryCenteredHeaderStyle}"/>
<DataGridTemplateColumn Header="Status" Width="130" HeaderStyle="{StaticResource HistoryCenteredHeaderStyle}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Style="{StaticResource StatusBadge}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Ellipse Width="12" Height="12" Fill="{Binding StatusDotBrush}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding StatusText}" Style="{StaticResource StatusText}"/>
</StackPanel>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<DockPanel Grid.Row="1" Margin="14,0" LastChildFill="False">
<TextBlock x:Name="RecordCountText" DockPanel.Dock="Left" VerticalAlignment="Center" Foreground="#52627A" FontSize="12"/>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Content="" Width="28" Height="24" Padding="0" Foreground="#A8B5C7" Background="White" BorderBrush="#E4EAF3"/>
<Button Content="1" Width="28" Height="24" Padding="0" Margin="6,0" Foreground="White" Background="{StaticResource PrimaryBrush}" BorderBrush="{StaticResource PrimaryBrush}"/>
<Button Content="" Width="28" Height="24" Padding="0" Foreground="#A8B5C7" Background="White" BorderBrush="#E4EAF3"/>
</StackPanel>
</DockPanel>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -1,17 +1,82 @@
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using HikvisionAttendanceManager.App.Models;
using HikvisionAttendanceManager.App.Services;
namespace HikvisionAttendanceManager.App;
public sealed class SyncHistoryRow
{
public SyncHistoryRow(SyncHistoryEntry entry)
{
Entry = entry;
}
public SyncHistoryEntry Entry { get; }
public DateTime StartedAt => Entry.StartedAt;
public string StartedAtText => Entry.StartedAt.ToString("yyyy-MM-dd HH:mm:ss");
public string OperationText => Entry.Operation switch
{
SyncOperationKind.AttendanceSync => "Attendance Sync",
SyncOperationKind.UserCreation => "User Sync",
SyncOperationKind.UserDeletion => "User Sync",
SyncOperationKind.DeviceToDb => "Template Sync",
SyncOperationKind.DbToDevice => "Template Sync",
SyncOperationKind.DepartmentalUserSync => "Departmental Sync",
SyncOperationKind.FaceEnrollment => "Template Sync",
_ => Entry.Operation.ToString()
};
public string Source => string.IsNullOrWhiteSpace(Entry.SourceDevice) ? "-" : Entry.SourceDevice;
public string Target => string.IsNullOrWhiteSpace(Entry.TargetDevice) ? "-" : Entry.TargetDevice;
public string Context => Entry.Context.Replace(" · ", Environment.NewLine).Replace(" · ", Environment.NewLine);
public int Total => Entry.Total;
public int Success => Entry.Success;
public int Failed => Entry.Failed;
public int Skipped => Entry.Skipped;
public bool IsFailed => Entry.Status.Contains("fail", StringComparison.OrdinalIgnoreCase) || Failed > 0 && Success == 0;
public string StatusText => IsFailed ? "Failed" : "Completed";
public string StatusDotBrush => IsFailed ? "#D93E5A" : "#17A164";
public string OperationIconBackground => Entry.Operation switch
{
SyncOperationKind.AttendanceSync => "#F0E8FF",
SyncOperationKind.DeviceToDb or SyncOperationKind.DbToDevice or SyncOperationKind.FaceEnrollment => "#E5FBFF",
SyncOperationKind.DepartmentalUserSync => "#FFF0E2",
_ => "#EAF1FF"
};
public string OperationIconBrush => Entry.Operation switch
{
SyncOperationKind.AttendanceSync => "#7C3AED",
SyncOperationKind.DeviceToDb or SyncOperationKind.DbToDevice or SyncOperationKind.FaceEnrollment => "#0891B2",
SyncOperationKind.DepartmentalUserSync => "#F97316",
_ => "#2563EB"
};
public string OperationIconData => Entry.Operation switch
{
SyncOperationKind.AttendanceSync => "M 7 8 A 3 3 0 1 1 7 2 A 3 3 0 0 1 7 8 M 2 15 C 2.8 11 11.2 11 12 15 M 14 6 V 10 M 12 8 H 16",
SyncOperationKind.DeviceToDb or SyncOperationKind.DbToDevice or SyncOperationKind.FaceEnrollment => "M 2 3 H 14 V 13 H 2 Z M 4 10 L 7 7 L 9 9 L 11 6 L 14 10 M 5 5.5 A 1.5 1.5 0 1 1 5 2.5 A 1.5 1.5 0 0 1 5 5.5",
SyncOperationKind.DepartmentalUserSync => "M 2 14 H 14 M 3 14 V 3 H 10 V 14 M 10 7 H 14 V 14 M 5 6 H 6 M 8 6 H 9 M 5 10 H 6 M 8 10 H 9",
_ => "M 7 8 A 3 3 0 1 1 7 2 A 3 3 0 0 1 7 8 M 2 15 C 2.8 11 11.2 11 12 15"
};
}
public partial class SyncHistoryView : UserControl
{
private readonly OperationHistoryService _history = new();
private readonly ObservableCollection<SyncHistoryRow> _rows = [];
private readonly List<SyncHistoryRow> _allRows = [];
private bool _initialized;
private bool _loadingFilters;
public SyncHistoryView()
{
InitializeComponent();
HistoryGrid.ItemsSource = _rows;
DateRangeCombo.ItemsSource = new[] { "Last 7 Days", "Last 30 Days", "All Dates" };
DateRangeCombo.SelectedIndex = 2;
}
public async Task InitializeAsync()
@ -23,5 +88,68 @@ public partial class SyncHistoryView : UserControl
private async void Refresh_Click(object sender, RoutedEventArgs e) => await RefreshAsync();
private async Task RefreshAsync() => HistoryGrid.ItemsSource = await _history.LoadAsync();
public async Task RefreshAsync()
{
var originalContent = RefreshButton.Content;
try
{
RefreshButton.IsEnabled = false;
RefreshButton.Content = "Refreshing...";
_allRows.Clear();
_rows.Clear();
var history = await _history.LoadAsync();
_allRows.AddRange(history.Select(entry => new SyncHistoryRow(entry)));
ResetFilters();
ApplyFilters();
}
finally
{
RefreshButton.IsEnabled = true;
RefreshButton.Content = originalContent;
}
}
private void ResetFilters()
{
_loadingFilters = true;
DateRangeCombo.SelectedIndex = 2;
OperationCombo.ItemsSource = new[] { "All Operations" }.Concat(_allRows.Select(row => row.OperationText).Distinct().Order()).ToList();
SourceCombo.ItemsSource = new[] { "All Sources" }.Concat(_allRows.Select(row => row.Source).Where(value => value != "-").Distinct().Order()).ToList();
TargetCombo.ItemsSource = new[] { "All Targets" }.Concat(_allRows.Select(row => row.Target).Where(value => value != "-").Distinct().Order()).ToList();
OperationCombo.SelectedIndex = 0;
SourceCombo.SelectedIndex = 0;
TargetCombo.SelectedIndex = 0;
_loadingFilters = false;
}
private void Filter_Changed(object sender, SelectionChangedEventArgs e)
{
if (_loadingFilters) return;
ApplyFilters();
}
private void ApplyFilters()
{
IEnumerable<SyncHistoryRow> query = _allRows;
query = (DateRangeCombo.SelectedItem as string) switch
{
"Last 7 Days" => query.Where(row => row.StartedAt >= DateTime.Today.AddDays(-6)),
"Last 30 Days" => query.Where(row => row.StartedAt >= DateTime.Today.AddDays(-29)),
_ => query
};
if (OperationCombo.SelectedItem is string operation && operation != "All Operations")
query = query.Where(row => row.OperationText == operation);
if (SourceCombo.SelectedItem is string source && source != "All Sources")
query = query.Where(row => row.Source == source);
if (TargetCombo.SelectedItem is string target && target != "All Targets")
query = query.Where(row => row.Target == target);
_rows.Clear();
foreach (var row in query.OrderByDescending(row => row.StartedAt))
_rows.Add(row);
RecordCountText.Text = $"{_rows.Count:N0} record(s) found";
}
}