feat(ui): implement Departmental User Sync and Sync History views
Departmental user sync and sync history pages, completing the workflow UI setmain
parent
425cb4954e
commit
202d490980
|
|
@ -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 & FILTERS" Style="{StaticResource CardCaption}"/>
|
||||
<Grid Margin="0,12,0,0">
|
||||
<TextBlock Text="TARGET & 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 & 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 & 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"/>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue