fix(ui): coalesce dashboard refresh and wire main navigation shell

Single-flight dashboard refresh, loading state in finally, and main-window navigation between workflow pages.
main
SYED MUSTUFA AHMED NAQVI 2026-08-27 11:10:48 +05:00
parent 27327aa580
commit be49cea934
2 changed files with 414 additions and 101 deletions

View File

@ -1,37 +1,198 @@
<Window x:Class="HikvisionAttendanceManager.App.MainWindow"
<Window x:Class="HikvisionAttendanceManager.App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HikvisionAttendanceManager.App"
Title="Hikvision Attendance Manager" Height="780" Width="1280" MinHeight="620" MinWidth="1020"
WindowStartupLocation="CenterScreen" Background="{StaticResource CanvasBrush}">
WindowStartupLocation="CenterScreen" WindowState="Maximized" Background="{StaticResource CanvasBrush}">
<Window.Resources>
<Style x:Key="DevicesHeaderButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="MinWidth" Value="106"/>
<Setter Property="Height" Value="38"/>
<Setter Property="Padding" Value="14,0"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style x:Key="DevicesTableHeaderStyle" TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#FBFCFE"/>
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Padding" Value="14,0"/>
<Setter Property="Height" Value="46"/>
<Setter Property="BorderBrush" Value="#E7ECF4"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridColumnHeader">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DevicesTableCellStyle" TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="14,0"/>
<Setter Property="Foreground" Value="#334155"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridCell">
<Border Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#EFF6FF"/>
<Setter Property="Foreground" Value="#1D4ED8"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="DevicesTextCell" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
</Style>
<Style x:Key="DeviceEditorLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#1F2A44"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,7"/>
</Style>
<Style x:Key="DeviceEditorTextBox" TargetType="TextBox">
<Setter Property="Height" Value="36"/>
<Setter Property="Padding" Value="12,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"/>
</Style>
<Style x:Key="DeviceEditorPasswordBox" TargetType="PasswordBox">
<Setter Property="Height" Value="36"/>
<Setter Property="Padding" Value="12,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"/>
</Style>
<Style x:Key="StatusBadgeBorder" TargetType="Border">
<Setter Property="Background" Value="#F3E8FF"/>
<Setter Property="BorderBrush" Value="#E9D5FF"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="5"/>
<Setter Property="Padding" Value="7,3"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="Online">
<Setter Property="Background" Value="#DCFCE7"/>
<Setter Property="BorderBrush" Value="#BBF7D0"/>
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="Offline">
<Setter Property="Background" Value="#FEE2E2"/>
<Setter Property="BorderBrush" Value="#FECACA"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="StatusBadgeText" TargetType="TextBlock">
<Setter Property="Foreground" Value="#7E22CE"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Text" Value="{Binding Status}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="Unknown">
<Setter Property="Text" Value="Not Tested"/>
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="Online">
<Setter Property="Foreground" Value="#15803D"/>
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="Offline">
<Setter Property="Foreground" Value="#B91C1C"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="205"/>
<ColumnDefinition Width="270"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,1,0">
<DockPanel Margin="12">
<DockPanel Margin="8,0,14,0">
<StackPanel DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Margin="7,12,7,26">
<Border Width="31" Height="35" CornerRadius="8" Background="#EAF1FF">
<TextBlock Text="⌂" Foreground="{StaticResource PrimaryBrush}" FontWeight="Bold" FontSize="18" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
<StackPanel Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock Text="Hikvision" FontWeight="Bold" FontSize="16" Foreground="#16213A"/>
<TextBlock Text="Attendance Manager" FontSize="10" Foreground="#77849A"/>
<StackPanel Orientation="Horizontal" Margin="18,6,0,44">
<Path Width="42" Height="42" Stretch="Uniform" Stroke="{StaticResource PrimaryBrush}" StrokeThickness="2.6" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,12,0" Data="M 16 2 L 30 7 V 17 C 30 26 23 31 16 34 C 9 31 2 26 2 17 V 7 Z M 10 17 L 14 21 L 23 12"/>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Hikvision" FontWeight="Bold" FontSize="22" Foreground="#16213A"/>
<TextBlock Text="Attendance Manager" FontSize="13" Foreground="#516079" Margin="1,1,0,0"/>
</StackPanel>
</StackPanel>
<Button x:Name="DashboardNav" Content="⌂ Dashboard" Style="{StaticResource SidebarButton}" Background="#EAF1FF" Foreground="#1D4ED8" Click="Navigate_Click" Tag="Dashboard"/>
<Button Content="▣ Devices" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Devices"/>
<Button Content="⇄ Attendance Sync" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Attendance Sync"/>
<Button Content="♙ User Management" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="User Management"/>
<TextBlock Text="TEMPLATE SYNCING" Margin="14,16,0,4" FontSize="10" FontWeight="SemiBold" Foreground="#94A3B8"/>
<Button Content=" ↓ Device → DB" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Device → DB"/>
<Button Content=" ↑ DB → Device" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="DB → Device"/>
<Button Content=" ▤ Departmental User Sync" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Departmental User Sync"/>
<Button Content="◷ Sync History" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Sync History"/>
<Button x:Name="DashboardNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Dashboard">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 3 11 L 11 4 L 19 11 M 5 10 V 19 H 17 V 10 M 9 19 V 14 H 13 V 19"/>
<TextBlock Text="Dashboard" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="DevicesNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Devices">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 4 5 H 18 V 15 H 4 Z M 9 19 H 13 M 11 15 V 19"/>
<TextBlock Text="Devices" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="AttendanceSyncNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Attendance Sync">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 4 8 H 17 M 14 5 L 17 8 L 14 11 M 18 16 H 5 M 8 13 L 5 16 L 8 19"/>
<TextBlock Text="Attendance Sync" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="UserManagementNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="User Management">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 11 11 A 4 4 0 1 1 11 3 A 4 4 0 0 1 11 11 M 4 20 C 5 15 17 15 18 20"/>
<TextBlock Text="User Management" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<TextBlock Text="TEMPLATE SYNCING" Margin="20,28,0,18" FontSize="12" FontWeight="SemiBold" Foreground="#718096"/>
<Button x:Name="DeviceToDbNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Device → DB">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 11 3 V 16 M 6 11 L 11 16 L 16 11 M 4 20 H 18"/>
<TextBlock Text="Device → DB" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="DbToDeviceNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="DB → Device">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 11 19 V 6 M 6 11 L 11 6 L 16 11 M 4 20 H 18"/>
<TextBlock Text="DB → Device" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="DepartmentalSyncNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Departmental User Sync">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.7" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,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" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="SyncHistoryNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Sync History">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 11 3 A 8 8 0 1 1 5 5.8 M 4 2 V 6 H 8 M 11 7 V 12 L 15 14"/>
<TextBlock Text="Sync History" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="20,0,20,14">
<Border Height="1" Background="#E6EBF3" Margin="0,0,0,16"/>
<Button x:Name="SettingsNav" Style="{StaticResource SidebarButton}" Padding="0" Click="Navigate_Click" Tag="Settings">
<StackPanel Orientation="Horizontal">
<Path Width="22" Height="22" Stretch="Uniform" Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" StrokeThickness="1.8" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="Transparent" Margin="0,0,20,0" Data="M 11 8 A 3 3 0 1 1 11 14 A 3 3 0 0 1 11 8 M 11 2 V 5 M 11 17 V 20 M 4.6 4.6 L 6.8 6.8 M 15.2 15.2 L 17.4 17.4 M 2 11 H 5 M 17 11 H 20 M 4.6 17.4 L 6.8 15.2 M 15.2 6.8 L 17.4 4.6"/>
<TextBlock Text="Settings" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<Button DockPanel.Dock="Bottom" Content="⚙ Settings" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Settings"/>
</DockPanel>
</Border>
@ -52,7 +213,7 @@
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Border Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" CornerRadius="18" Padding="10,5" Margin="0,0,10,0"><TextBlock Text="● Operator" Foreground="#475569" FontSize="11"/></Border>
<Button x:Name="RefreshDashboardButton" Content="↻ Refresh" Click="RefreshDashboard_Click"/>
<Button x:Name="RefreshDashboardButton" Content="↻ Refresh" Style="{StaticResource HeaderRefreshButton}" Click="RefreshDashboard_Click"/>
</StackPanel>
<TextBlock x:Name="DashboardErrorText" Grid.Row="0" Grid.ColumnSpan="2" Margin="0,52,0,0" Foreground="#DC2626" FontSize="12" Visibility="Collapsed" TextWrapping="Wrap"/>
<UniformGrid Grid.Row="1" Grid.ColumnSpan="2" Columns="4" Margin="0,28,0,20">
@ -87,52 +248,129 @@
</Grid>
<Grid x:Name="DevicesPage" Visibility="Collapsed">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<DockPanel>
<StackPanel DockPanel.Dock="Left"><TextBlock Text="Devices" FontSize="25" FontWeight="SemiBold" Foreground="#17233B"/><TextBlock Text="Manage Hikvision terminals and test their connectivity." Margin="0,6,0,0" Foreground="#66758C"/></StackPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="Refresh" Margin="0,0,8,0" Click="RefreshDevices_Click"/>
<Button Content="+ Add Device" Style="{StaticResource PrimaryButton}" Click="AddDevice_Click"/>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Devices" FontSize="23" FontWeight="SemiBold" Foreground="#17233B"/>
<TextBlock Text="Manage Hikvision terminals and test their connectivity." Margin="0,8,0,0" Foreground="#66758C" FontSize="12"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
<Button x:Name="RefreshDevicesButton" Content="↻ Refresh" Style="{StaticResource HeaderRefreshButton}" Margin="0,0,14,0" Click="RefreshDevices_Click"/>
<Button Content="+ Add Device" Style="{StaticResource PrimaryButton}" Height="38" MinWidth="126" Padding="16,0" FontSize="12" Click="AddDevice_Click"/>
</StackPanel>
</Grid>
<Border Grid.Row="1" x:Name="DeviceEditor" Visibility="Collapsed" Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" CornerRadius="8" Margin="0,24,0,16" Padding="22">
<StackPanel>
<TextBlock Text="Add Manual Hikvision Device" FontWeight="SemiBold" FontSize="15" Foreground="#17233B" Margin="0,0,0,18"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.4*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="0.95*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="1.15*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="1.1*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Machine Name" Style="{StaticResource DeviceEditorLabel}"/>
<TextBox x:Name="DeviceNameBox" Style="{StaticResource DeviceEditorTextBox}" ToolTip="Machine name"/>
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Machine ID" Style="{StaticResource DeviceEditorLabel}"/>
<TextBox x:Name="MachineIdBox" Style="{StaticResource DeviceEditorTextBox}" ToolTip="Machine ID"/>
</StackPanel>
<StackPanel Grid.Column="4">
<TextBlock Text="IP Address" Style="{StaticResource DeviceEditorLabel}"/>
<TextBox x:Name="IpAddressBox" Style="{StaticResource DeviceEditorTextBox}" ToolTip="IP Address"/>
</StackPanel>
<StackPanel Grid.Column="6">
<TextBlock Text="Port" Style="{StaticResource DeviceEditorLabel}"/>
<TextBox x:Name="PortBox" Style="{StaticResource DeviceEditorTextBox}" Text="8000" ToolTip="Port"/>
</StackPanel>
<StackPanel Grid.Column="8">
<TextBlock Text="Username" Style="{StaticResource DeviceEditorLabel}"/>
<TextBox x:Name="UsernameBox" Style="{StaticResource DeviceEditorTextBox}" ToolTip="Username"/>
</StackPanel>
<StackPanel Grid.Column="10">
<TextBlock Text="Password" Style="{StaticResource DeviceEditorLabel}"/>
<PasswordBox x:Name="PasswordBox" Style="{StaticResource DeviceEditorPasswordBox}" ToolTip="Password"/>
</StackPanel>
<StackPanel Grid.Column="12">
<TextBlock Text="Model" Style="{StaticResource DeviceEditorLabel}"/>
<TextBox x:Name="ModelBox" Style="{StaticResource DeviceEditorTextBox}" ToolTip="Model"/>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,18,0,0">
<Button x:Name="EditorTestConnectionButton" Content="Test Connection" Height="34" MinWidth="132" Margin="0,0,10,0" Click="TestEditorConnection_Click"/>
<Button Content="Add Device" Style="{StaticResource PrimaryButton}" Height="34" MinWidth="118" Margin="0,0,10,0" Click="SaveDevice_Click"/>
<Button Content="Cancel" Height="34" MinWidth="86" Click="CancelEdit_Click"/>
</StackPanel>
</StackPanel>
</DockPanel>
<Border Grid.Row="1" x:Name="DeviceEditor" Visibility="Collapsed" Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" CornerRadius="10" Margin="0,24,0,16" Padding="18">
<StackPanel><TextBlock Text="Add Manual Hikvision Device" FontWeight="SemiBold" Margin="0,0,0,10"/>
<WrapPanel>
<TextBox x:Name="DeviceNameBox" Width="180" Margin="0,0,10,8" ToolTip="Machine name"/>
<TextBox x:Name="MachineIdBox" Width="120" Margin="0,0,10,8" ToolTip="Machine ID"/>
<TextBox x:Name="IpAddressBox" Width="140" Margin="0,0,10,8" ToolTip="IP Address"/>
<TextBox x:Name="PortBox" Width="75" Margin="0,0,10,8" Text="8000" ToolTip="Port"/>
<TextBox x:Name="UsernameBox" Width="120" Margin="0,0,10,8" ToolTip="Username"/>
<PasswordBox x:Name="PasswordBox" Width="120" Margin="0,0,10,8" ToolTip="Password"/>
<TextBox x:Name="ModelBox" Width="140" Margin="0,0,10,8" ToolTip="Model"/>
<Button x:Name="EditorTestConnectionButton" Content="Test Connection" Margin="0,0,10,8" Click="TestEditorConnection_Click"/>
<Button Content="Add Device" Style="{StaticResource PrimaryButton}" Margin="0,0,8,8" Click="SaveDevice_Click"/>
<Button Content="Cancel" Margin="0,0,0,8" Click="CancelEdit_Click"/>
</WrapPanel></StackPanel>
</Border>
<Border Grid.Row="2" Style="{StaticResource CardStyle}" Padding="0">
<Border Grid.Row="2" Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" CornerRadius="8" Margin="0,24,0,0" Padding="0" SnapsToDevicePixels="True">
<Grid>
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="16">
<Button x:Name="DevicesTestConnectionButton" Content="Test Connection" Style="{StaticResource PrimaryButton}" Click="TestConnection_Click"/>
<Button Content="View Device Information" Margin="8,0,0,0" Click="DeviceInformation_Click"/>
<Button Content="Edit" Margin="8,0,0,0" Click="EditDevice_Click"/>
<Button Content="Remove" Margin="8,0,0,0" Foreground="#B91C1C" Click="RemoveDevice_Click"/>
<TextBlock x:Name="ConnectionMessageText" VerticalAlignment="Center" Margin="18,0,0,0" Foreground="#66758C"/>
<Button x:Name="DevicesTestConnectionButton" Content="Test Connection" Style="{StaticResource PrimaryButton}" Height="34" Click="TestConnection_Click"/>
<Button Content="View Device Information" Height="34" Margin="8,0,0,0" Click="DeviceInformation_Click"/>
<Button Content="Edit" Height="34" Margin="8,0,0,0" Click="EditDevice_Click"/>
<Button Content="Remove" Height="34" Margin="8,0,0,0" Foreground="#B91C1C" Click="RemoveDevice_Click"/>
<TextBlock x:Name="ConnectionMessageText" VerticalAlignment="Center" Margin="18,0,0,0" Foreground="#66758C" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>
<DataGrid x:Name="DevicesGrid" ItemsSource="{Binding Devices}" SelectedItem="{Binding SelectedDevice, Mode=TwoWay}" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True" BorderThickness="0" HeadersVisibility="Column" RowHeight="44">
<DataGrid x:Name="DevicesGrid" ItemsSource="{Binding Devices}" SelectedItem="{Binding SelectedDevice, Mode=TwoWay}" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True" BorderThickness="0" HeadersVisibility="Column" RowHeight="52" GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#EEF2F7" Background="White" RowBackground="White" AlternatingRowBackground="White" ColumnHeaderStyle="{StaticResource DevicesTableHeaderStyle}" CellStyle="{StaticResource DevicesTableCellStyle}" MinWidth="980" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto">
<DataGrid.Resources>
<Style TargetType="DataGridRow">
<Setter Property="BorderBrush" Value="#EEF2F7"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Background" Value="White"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="Machine Name" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="Machine ID" Binding="{Binding MachineId}" Width="110"/>
<DataGridTextColumn Header="IP Address" Binding="{Binding IpAddress}" Width="140"/>
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="70"/>
<DataGridTextColumn Header="Model" Binding="{Binding Model}" Width="150"/>
<DataGridTextColumn Header="Source" Binding="{Binding Source}" Width="80"/>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="120"/>
<DataGridTextColumn Header="Registered Users" Binding="{Binding RegisteredUserCountDisplay}" Width="120"/>
<DataGridTextColumn Header="Last Sync" Binding="{Binding LastSyncDisplay}" Width="150"/>
<DataGridTextColumn Header="Machine Name" Binding="{Binding Name}" Width="1.45*" ElementStyle="{StaticResource DevicesTextCell}"/>
<DataGridTextColumn Header="Machine ID" Binding="{Binding MachineId}" Width="135" ElementStyle="{StaticResource DevicesTextCell}"/>
<DataGridTextColumn Header="IP Address" Binding="{Binding IpAddress}" Width="130" ElementStyle="{StaticResource DevicesTextCell}"/>
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="70" ElementStyle="{StaticResource DevicesTextCell}"/>
<DataGridTextColumn Header="Model" Binding="{Binding Model}" Width="115" ElementStyle="{StaticResource DevicesTextCell}"/>
<DataGridTextColumn Header="Source" Binding="{Binding Source}" Width="80" ElementStyle="{StaticResource DevicesTextCell}"/>
<DataGridTemplateColumn Header="Status" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Style="{StaticResource StatusBadgeBorder}">
<TextBlock Style="{StaticResource StatusBadgeText}"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Registered Users" Binding="{Binding RegisteredUserCountDisplay}" Width="130" ElementStyle="{StaticResource DevicesTextCell}"/>
<DataGridTextColumn Header="Last Sync" Binding="{Binding LastSyncDisplay}" Width="155" ElementStyle="{StaticResource DevicesTextCell}"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
<Border x:Name="DevicesEmptyState" Visibility="Collapsed" Background="White" Margin="1,47,1,60">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="No Hikvision devices found" FontSize="16" FontWeight="SemiBold" Foreground="#17233B" HorizontalAlignment="Center"/>
<TextBlock Text="Add a device or refresh to load devices from HRMS." Margin="0,8,0,0" FontSize="12" Foreground="#66758C" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<Border x:Name="DevicesLoadingState" Visibility="Collapsed" Background="#D9FFFFFF" Margin="1,47,1,60">
<TextBlock Text="Loading devices..." FontSize="13" FontWeight="SemiBold" Foreground="#66758C" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</Border>
</Grid>
@ -163,3 +401,4 @@
</Grid>
</Grid>
</Window>

View File

@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using HikvisionAttendanceManager.App.Models;
using HikvisionAttendanceManager.App.Services;
@ -12,6 +13,7 @@ public partial class MainWindow : Window
private readonly DeviceConnectionService _connectionService = new();
private readonly DashboardService _dashboardService = new();
private readonly SemaphoreSlim _dashboardRefreshLock = new(1, 1);
private Task? _dashboardRefreshTask;
private Device? _editingDevice;
public DashboardViewModel Dashboard { get; } = new();
@ -22,6 +24,8 @@ public partial class MainWindow : Window
{
InitializeComponent();
DataContext = this;
SetActiveNavigation("Dashboard");
UpdateDevicesVisualState(isLoading: true);
AppLogger.Info("[STARTUP] MainWindow created");
ContentRendered += MainWindow_ContentRendered;
}
@ -43,6 +47,7 @@ public partial class MainWindow : Window
Devices = await Task.Run(async () => await _deviceService.LoadUnifiedAsync(timeout.Token), timeout.Token);
DevicesGrid.ItemsSource = Devices;
ConnectionMessageText.Text = Devices.Count == 0 ? "No devices available from HRMS or manual storage." : "";
UpdateDevicesVisualState(isLoading: false);
AppLogger.Info("[STARTUP] Device loading completed");
}
catch (OperationCanceledException)
@ -50,6 +55,7 @@ public partial class MainWindow : Window
Devices = [];
DevicesGrid.ItemsSource = Devices;
ConnectionMessageText.Text = "HRMS: Connection timed out. You can retry from Devices.";
UpdateDevicesVisualState(isLoading: false);
AppLogger.Warning("[STARTUP] HRMS initialization timed out.");
}
catch (Exception ex)
@ -58,6 +64,7 @@ public partial class MainWindow : Window
Devices = [];
DevicesGrid.ItemsSource = Devices;
ConnectionMessageText.Text = "Unable to load HRMS devices. See the application log for details.";
UpdateDevicesVisualState(isLoading: false);
}
await LoadDashboardAsync();
AppLogger.Info("[STARTUP] HRMS initialization completed");
@ -66,39 +73,19 @@ public partial class MainWindow : Window
private Task LoadDashboardAsync() => RefreshDashboardAsync(reloadDevices: false);
private void Navigate_Click(object sender, RoutedEventArgs e)
private Task RefreshDashboardAsync(bool reloadDevices = false)
{
var page = (string)((Button)sender).Tag;
DashboardPage.Visibility = page == "Dashboard" ? Visibility.Visible : Visibility.Collapsed;
DevicesPage.Visibility = page == "Devices" ? Visibility.Visible : Visibility.Collapsed;
UserManagementPage.Visibility = page == "User Management" ? Visibility.Visible : Visibility.Collapsed;
AttendanceSyncPage.Visibility = page == "Attendance Sync" ? Visibility.Visible : Visibility.Collapsed;
DeviceToDbPage.Visibility = page == "Device → DB" ? Visibility.Visible : Visibility.Collapsed;
DbToDevicePage.Visibility = page == "DB → Device" ? Visibility.Visible : Visibility.Collapsed;
DepartmentalSyncPage.Visibility = page == "Departmental User Sync" ? Visibility.Visible : Visibility.Collapsed;
SyncHistoryPage.Visibility = page == "Sync History" ? Visibility.Visible : Visibility.Collapsed;
PlaceholderPage.Visibility = page == "Settings" ? Visibility.Visible : Visibility.Collapsed;
var inFlight = _dashboardRefreshTask;
if (inFlight is { IsCompleted: false })
return inFlight;
if (PlaceholderPage.Visibility == Visibility.Visible)
PlaceholderTitle.Text = page;
_ = page switch
{
"User Management" => UserManagementControl.InitializeAsync(),
"Attendance Sync" => AttendanceSyncControl.InitializeAsync(),
"Device → DB" => DeviceToDbControl.InitializeAsync(),
"DB → Device" => DbToDeviceControl.InitializeAsync(),
"Departmental User Sync" => DepartmentalSyncControl.InitializeAsync(),
"Sync History" => SyncHistoryControl.InitializeAsync(),
"Dashboard" => LoadDashboardAsync(),
_ => Task.CompletedTask
};
_dashboardRefreshTask = RefreshDashboardCoreAsync(reloadDevices);
return _dashboardRefreshTask;
}
private async Task RefreshDashboardAsync(bool reloadDevices = false)
private async Task RefreshDashboardCoreAsync(bool reloadDevices)
{
if (!await _dashboardRefreshLock.WaitAsync(0).ConfigureAwait(true))
return;
await _dashboardRefreshLock.WaitAsync().ConfigureAwait(true);
var originalButtonContent = RefreshDashboardButton.Content;
try
@ -112,6 +99,7 @@ public partial class MainWindow : Window
{
Devices = await _deviceService.LoadUnifiedAsync().ConfigureAwait(true);
DevicesGrid.ItemsSource = Devices;
UpdateDevicesVisualState(isLoading: false);
DashboardErrorText.Visibility = Visibility.Collapsed;
}
catch (Exception ex)
@ -138,8 +126,22 @@ public partial class MainWindow : Window
}
Dashboard.IsLoadingConnectivity = true;
await _dashboardService.ProbeConnectivityAsync(Devices.ToList()).ConfigureAwait(true);
Dashboard.IsLoadingConnectivity = false;
using var probeTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(DashboardService.ProbeTimeoutSeconds));
try
{
var probeResults = await Task.Run(async () =>
await _dashboardService.ProbeConnectivityAsync(Devices.ToList(), probeTimeout.Token).ConfigureAwait(false)
).ConfigureAwait(true);
DashboardService.ApplyProbeResults(probeResults);
}
catch (OperationCanceledException)
{
AppLogger.Warning("Dashboard connectivity probe timed out before all devices were checked.");
}
Dashboard.EndConnectivityRefresh();
Dashboard.UpdateFromDevices(Devices);
var history = await _dashboardService.LoadRecentHistoryAsync().ConfigureAwait(true);
@ -161,18 +163,49 @@ public partial class MainWindow : Window
DashboardErrorText.Text = "Unable to load device data.\nCheck the HRMS connection.";
DashboardErrorText.Visibility = Visibility.Visible;
Dashboard.IsInitialLoad = false;
Dashboard.IsLoadingConnectivity = false;
Dashboard.EndConnectivityRefresh();
Dashboard.UpdateFromDevices(Devices);
}
finally
{
Dashboard.IsLoadingConnectivity = false;
Dashboard.EndConnectivityRefresh();
Dashboard.UpdateFromDevices(Devices);
RefreshDashboardButton.IsEnabled = true;
RefreshDashboardButton.Content = originalButtonContent;
_dashboardRefreshLock.Release();
}
}
private void Navigate_Click(object sender, RoutedEventArgs e)
{
var page = (string)((Button)sender).Tag;
SetActiveNavigation(page);
DashboardPage.Visibility = page == "Dashboard" ? Visibility.Visible : Visibility.Collapsed;
DevicesPage.Visibility = page == "Devices" ? Visibility.Visible : Visibility.Collapsed;
UserManagementPage.Visibility = page == "User Management" ? Visibility.Visible : Visibility.Collapsed;
AttendanceSyncPage.Visibility = page == "Attendance Sync" ? Visibility.Visible : Visibility.Collapsed;
DeviceToDbPage.Visibility = page == "Device → DB" ? Visibility.Visible : Visibility.Collapsed;
DbToDevicePage.Visibility = page == "DB → Device" ? Visibility.Visible : Visibility.Collapsed;
DepartmentalSyncPage.Visibility = page == "Departmental User Sync" ? Visibility.Visible : Visibility.Collapsed;
SyncHistoryPage.Visibility = page == "Sync History" ? Visibility.Visible : Visibility.Collapsed;
PlaceholderPage.Visibility = page == "Settings" ? Visibility.Visible : Visibility.Collapsed;
if (PlaceholderPage.Visibility == Visibility.Visible)
PlaceholderTitle.Text = page;
_ = page switch
{
"User Management" => UserManagementControl.InitializeAsync(),
"Attendance Sync" => AttendanceSyncControl.InitializeAsync(),
"Device → DB" => DeviceToDbControl.InitializeAsync(),
"DB → Device" => DbToDeviceControl.InitializeAsync(),
"Departmental User Sync" => DepartmentalSyncControl.InitializeAsync(),
"Sync History" => SyncHistoryControl.InitializeAsync(),
"Dashboard" => Task.CompletedTask,
_ => Task.CompletedTask
};
}
private void RefreshDashboard() => _ = LoadDashboardAsync();
private async void RefreshDashboard_Click(object sender, RoutedEventArgs e) =>
@ -182,8 +215,17 @@ public partial class MainWindow : Window
private async Task ReloadDevicesAsync()
{
var originalContent = RefreshDevicesButton.Content;
try
{
RefreshDevicesButton.IsEnabled = false;
RefreshDevicesButton.Content = "Loading...";
ClearDeviceEditor();
SelectedDevice = null;
DevicesGrid.SelectedItem = null;
ConnectionMessageText.Text = "";
ConnectionMessageText.Foreground = Brushes.Gray;
UpdateDevicesVisualState(isLoading: true);
Devices = await _deviceService.LoadUnifiedAsync();
ConnectionMessageText.Text = "Loaded latest devices from HRMS and manual storage.";
}
@ -191,11 +233,17 @@ public partial class MainWindow : Window
{
ConnectionMessageText.Text = "Unable to load HRMS devices: " + ex.Message;
}
finally
{
DevicesGrid.ItemsSource = Devices;
UpdateDevicesVisualState(isLoading: false);
RefreshDevicesButton.IsEnabled = true;
RefreshDevicesButton.Content = originalContent;
}
await LoadDashboardAsync();
}
private void AddDevice_Click(object sender, RoutedEventArgs e)
private void ClearDeviceEditor()
{
_editingDevice = null;
DeviceNameBox.Text = "";
@ -205,6 +253,32 @@ public partial class MainWindow : Window
UsernameBox.Text = "";
PasswordBox.Clear();
ModelBox.Text = "";
DeviceEditor.Visibility = Visibility.Collapsed;
}
private void UpdateDevicesVisualState(bool isLoading)
{
DevicesLoadingState.Visibility = isLoading ? Visibility.Visible : Visibility.Collapsed;
DevicesEmptyState.Visibility = !isLoading && Devices.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
private void SetActiveNavigation(string page)
{
foreach (var button in new[]
{
DashboardNav, DevicesNav, AttendanceSyncNav, UserManagementNav, DeviceToDbNav,
DbToDeviceNav, DepartmentalSyncNav, SyncHistoryNav, SettingsNav
})
{
var isActive = string.Equals(button.Tag as string, page, StringComparison.Ordinal);
button.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(isActive ? "#EAF1FF" : "Transparent"));
button.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(isActive ? "#1D4ED8" : "#516079"));
}
}
private void AddDevice_Click(object sender, RoutedEventArgs e)
{
ClearDeviceEditor();
DeviceEditor.Visibility = Visibility.Visible;
DeviceNameBox.Focus();
}
@ -286,7 +360,7 @@ public partial class MainWindow : Window
try
{
var result = await _connectionService.TestAsync(SelectedDevice!, CancellationToken.None).ConfigureAwait(true);
SelectedDevice!.Status = result.IsConnected ? "Online" : "Offline";
SelectedDevice!.Status = result.Outcome.ToDashboardStatus();
ConnectionMessageText.Text = result.IsConnected ? "✓ " + result.Message : "✕ Connection failed\n" + result.Message;
ConnectionMessageText.Foreground = result.IsConnected
? System.Windows.Media.Brushes.ForestGreen