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
parent
27327aa580
commit
be49cea934
|
|
@ -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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:HikvisionAttendanceManager.App"
|
xmlns:local="clr-namespace:HikvisionAttendanceManager.App"
|
||||||
Title="Hikvision Attendance Manager" Height="780" Width="1280" MinHeight="620" MinWidth="1020"
|
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>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="205"/>
|
<ColumnDefinition Width="270"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Border Grid.Column="0" Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,1,0">
|
<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 DockPanel.Dock="Top">
|
||||||
<StackPanel Orientation="Horizontal" Margin="7,12,7,26">
|
<StackPanel Orientation="Horizontal" Margin="18,6,0,44">
|
||||||
<Border Width="31" Height="35" CornerRadius="8" Background="#EAF1FF">
|
<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"/>
|
||||||
<TextBlock Text="⌂" Foreground="{StaticResource PrimaryBrush}" FontWeight="Bold" FontSize="18" VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
<StackPanel VerticalAlignment="Center">
|
||||||
</Border>
|
<TextBlock Text="Hikvision" FontWeight="Bold" FontSize="22" Foreground="#16213A"/>
|
||||||
<StackPanel Margin="10,0,0,0" VerticalAlignment="Center">
|
<TextBlock Text="Attendance Manager" FontSize="13" Foreground="#516079" Margin="1,1,0,0"/>
|
||||||
<TextBlock Text="Hikvision" FontWeight="Bold" FontSize="16" Foreground="#16213A"/>
|
|
||||||
<TextBlock Text="Attendance Manager" FontSize="10" Foreground="#77849A"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button x:Name="DashboardNav" Content="⌂ Dashboard" Style="{StaticResource SidebarButton}" Background="#EAF1FF" Foreground="#1D4ED8" Click="Navigate_Click" Tag="Dashboard"/>
|
<Button x:Name="DashboardNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Dashboard">
|
||||||
<Button Content="▣ Devices" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Devices"/>
|
<StackPanel Orientation="Horizontal">
|
||||||
<Button Content="⇄ Attendance Sync" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Attendance Sync"/>
|
<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"/>
|
||||||
<Button Content="♙ User Management" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="User Management"/>
|
<TextBlock Text="Dashboard" VerticalAlignment="Center"/>
|
||||||
<TextBlock Text="TEMPLATE SYNCING" Margin="14,16,0,4" FontSize="10" FontWeight="SemiBold" Foreground="#94A3B8"/>
|
</StackPanel>
|
||||||
<Button Content=" ↓ Device → DB" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Device → DB"/>
|
</Button>
|
||||||
<Button Content=" ↑ DB → Device" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="DB → Device"/>
|
<Button x:Name="DevicesNav" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Devices">
|
||||||
<Button Content=" ▤ Departmental User Sync" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Departmental User Sync"/>
|
<StackPanel Orientation="Horizontal">
|
||||||
<Button Content="◷ Sync History" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Sync History"/>
|
<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>
|
</StackPanel>
|
||||||
<Button DockPanel.Dock="Bottom" Content="⚙ Settings" Style="{StaticResource SidebarButton}" Click="Navigate_Click" Tag="Settings"/>
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|
@ -52,7 +213,7 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
<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>
|
<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>
|
</StackPanel>
|
||||||
<TextBlock x:Name="DashboardErrorText" Grid.Row="0" Grid.ColumnSpan="2" Margin="0,52,0,0" Foreground="#DC2626" FontSize="12" Visibility="Collapsed" TextWrapping="Wrap"/>
|
<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">
|
<UniformGrid Grid.Row="1" Grid.ColumnSpan="2" Columns="4" Margin="0,28,0,20">
|
||||||
|
|
@ -87,52 +248,129 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid x:Name="DevicesPage" Visibility="Collapsed">
|
<Grid x:Name="DevicesPage" Visibility="Collapsed">
|
||||||
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<DockPanel>
|
<RowDefinition Height="Auto"/>
|
||||||
<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>
|
<RowDefinition Height="Auto"/>
|
||||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
|
<RowDefinition Height="*"/>
|
||||||
<Button Content="Refresh" Margin="0,0,8,0" Click="RefreshDevices_Click"/>
|
</Grid.RowDefinitions>
|
||||||
<Button Content="+ Add Device" Style="{StaticResource PrimaryButton}" Click="AddDevice_Click"/>
|
<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>
|
||||||
</DockPanel>
|
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||||
<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">
|
<Button x:Name="RefreshDevicesButton" Content="↻ Refresh" Style="{StaticResource HeaderRefreshButton}" Margin="0,0,14,0" Click="RefreshDevices_Click"/>
|
||||||
<StackPanel><TextBlock Text="Add Manual Hikvision Device" FontWeight="SemiBold" Margin="0,0,0,10"/>
|
<Button Content="+ Add Device" Style="{StaticResource PrimaryButton}" Height="38" MinWidth="126" Padding="16,0" FontSize="12" Click="AddDevice_Click"/>
|
||||||
<WrapPanel>
|
</StackPanel>
|
||||||
<TextBox x:Name="DeviceNameBox" Width="180" Margin="0,0,10,8" ToolTip="Machine name"/>
|
</Grid>
|
||||||
<TextBox x:Name="MachineIdBox" Width="120" Margin="0,0,10,8" ToolTip="Machine ID"/>
|
<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">
|
||||||
<TextBox x:Name="IpAddressBox" Width="140" Margin="0,0,10,8" ToolTip="IP Address"/>
|
<StackPanel>
|
||||||
<TextBox x:Name="PortBox" Width="75" Margin="0,0,10,8" Text="8000" ToolTip="Port"/>
|
<TextBlock Text="Add Manual Hikvision Device" FontWeight="SemiBold" FontSize="15" Foreground="#17233B" Margin="0,0,0,18"/>
|
||||||
<TextBox x:Name="UsernameBox" Width="120" Margin="0,0,10,8" ToolTip="Username"/>
|
<Grid>
|
||||||
<PasswordBox x:Name="PasswordBox" Width="120" Margin="0,0,10,8" ToolTip="Password"/>
|
<Grid.ColumnDefinitions>
|
||||||
<TextBox x:Name="ModelBox" Width="140" Margin="0,0,10,8" ToolTip="Model"/>
|
<ColumnDefinition Width="1.4*"/>
|
||||||
<Button x:Name="EditorTestConnectionButton" Content="Test Connection" Margin="0,0,10,8" Click="TestEditorConnection_Click"/>
|
<ColumnDefinition Width="14"/>
|
||||||
<Button Content="Add Device" Style="{StaticResource PrimaryButton}" Margin="0,0,8,8" Click="SaveDevice_Click"/>
|
<ColumnDefinition Width="0.95*"/>
|
||||||
<Button Content="Cancel" Margin="0,0,0,8" Click="CancelEdit_Click"/>
|
<ColumnDefinition Width="14"/>
|
||||||
</WrapPanel></StackPanel>
|
<ColumnDefinition Width="1.15*"/>
|
||||||
</Border>
|
<ColumnDefinition Width="14"/>
|
||||||
<Border Grid.Row="2" Style="{StaticResource CardStyle}" Padding="0">
|
<ColumnDefinition Width="80"/>
|
||||||
<DockPanel>
|
<ColumnDefinition Width="14"/>
|
||||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="16">
|
<ColumnDefinition Width="1*"/>
|
||||||
<Button x:Name="DevicesTestConnectionButton" Content="Test Connection" Style="{StaticResource PrimaryButton}" Click="TestConnection_Click"/>
|
<ColumnDefinition Width="14"/>
|
||||||
<Button Content="View Device Information" Margin="8,0,0,0" Click="DeviceInformation_Click"/>
|
<ColumnDefinition Width="1*"/>
|
||||||
<Button Content="Edit" Margin="8,0,0,0" Click="EditDevice_Click"/>
|
<ColumnDefinition Width="14"/>
|
||||||
<Button Content="Remove" Margin="8,0,0,0" Foreground="#B91C1C" Click="RemoveDevice_Click"/>
|
<ColumnDefinition Width="1.1*"/>
|
||||||
<TextBlock x:Name="ConnectionMessageText" VerticalAlignment="Center" Margin="18,0,0,0" Foreground="#66758C"/>
|
</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>
|
||||||
<DataGrid x:Name="DevicesGrid" ItemsSource="{Binding Devices}" SelectedItem="{Binding SelectedDevice, Mode=TwoWay}" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True" BorderThickness="0" HeadersVisibility="Column" RowHeight="44">
|
</StackPanel>
|
||||||
<DataGrid.Columns>
|
</Border>
|
||||||
<DataGridTextColumn Header="Machine Name" Binding="{Binding Name}" Width="*"/>
|
<Border Grid.Row="2" Background="White" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" CornerRadius="8" Margin="0,24,0,0" Padding="0" SnapsToDevicePixels="True">
|
||||||
<DataGridTextColumn Header="Machine ID" Binding="{Binding MachineId}" Width="110"/>
|
<Grid>
|
||||||
<DataGridTextColumn Header="IP Address" Binding="{Binding IpAddress}" Width="140"/>
|
<DockPanel>
|
||||||
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="70"/>
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="16">
|
||||||
<DataGridTextColumn Header="Model" Binding="{Binding Model}" Width="150"/>
|
<Button x:Name="DevicesTestConnectionButton" Content="Test Connection" Style="{StaticResource PrimaryButton}" Height="34" Click="TestConnection_Click"/>
|
||||||
<DataGridTextColumn Header="Source" Binding="{Binding Source}" Width="80"/>
|
<Button Content="View Device Information" Height="34" Margin="8,0,0,0" Click="DeviceInformation_Click"/>
|
||||||
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="120"/>
|
<Button Content="Edit" Height="34" Margin="8,0,0,0" Click="EditDevice_Click"/>
|
||||||
<DataGridTextColumn Header="Registered Users" Binding="{Binding RegisteredUserCountDisplay}" Width="120"/>
|
<Button Content="Remove" Height="34" Margin="8,0,0,0" Foreground="#B91C1C" Click="RemoveDevice_Click"/>
|
||||||
<DataGridTextColumn Header="Last Sync" Binding="{Binding LastSyncDisplay}" Width="150"/>
|
<TextBlock x:Name="ConnectionMessageText" VerticalAlignment="Center" Margin="18,0,0,0" Foreground="#66758C" FontSize="12" TextWrapping="Wrap"/>
|
||||||
</DataGrid.Columns>
|
</StackPanel>
|
||||||
</DataGrid>
|
<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">
|
||||||
</DockPanel>
|
<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="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>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
@ -163,3 +401,4 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
using HikvisionAttendanceManager.App.Models;
|
using HikvisionAttendanceManager.App.Models;
|
||||||
using HikvisionAttendanceManager.App.Services;
|
using HikvisionAttendanceManager.App.Services;
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ public partial class MainWindow : Window
|
||||||
private readonly DeviceConnectionService _connectionService = new();
|
private readonly DeviceConnectionService _connectionService = new();
|
||||||
private readonly DashboardService _dashboardService = new();
|
private readonly DashboardService _dashboardService = new();
|
||||||
private readonly SemaphoreSlim _dashboardRefreshLock = new(1, 1);
|
private readonly SemaphoreSlim _dashboardRefreshLock = new(1, 1);
|
||||||
|
private Task? _dashboardRefreshTask;
|
||||||
private Device? _editingDevice;
|
private Device? _editingDevice;
|
||||||
|
|
||||||
public DashboardViewModel Dashboard { get; } = new();
|
public DashboardViewModel Dashboard { get; } = new();
|
||||||
|
|
@ -22,6 +24,8 @@ public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
SetActiveNavigation("Dashboard");
|
||||||
|
UpdateDevicesVisualState(isLoading: true);
|
||||||
AppLogger.Info("[STARTUP] MainWindow created");
|
AppLogger.Info("[STARTUP] MainWindow created");
|
||||||
ContentRendered += MainWindow_ContentRendered;
|
ContentRendered += MainWindow_ContentRendered;
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +47,7 @@ public partial class MainWindow : Window
|
||||||
Devices = await Task.Run(async () => await _deviceService.LoadUnifiedAsync(timeout.Token), timeout.Token);
|
Devices = await Task.Run(async () => await _deviceService.LoadUnifiedAsync(timeout.Token), timeout.Token);
|
||||||
DevicesGrid.ItemsSource = Devices;
|
DevicesGrid.ItemsSource = Devices;
|
||||||
ConnectionMessageText.Text = Devices.Count == 0 ? "No devices available from HRMS or manual storage." : "";
|
ConnectionMessageText.Text = Devices.Count == 0 ? "No devices available from HRMS or manual storage." : "";
|
||||||
|
UpdateDevicesVisualState(isLoading: false);
|
||||||
AppLogger.Info("[STARTUP] Device loading completed");
|
AppLogger.Info("[STARTUP] Device loading completed");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|
@ -50,6 +55,7 @@ public partial class MainWindow : Window
|
||||||
Devices = [];
|
Devices = [];
|
||||||
DevicesGrid.ItemsSource = Devices;
|
DevicesGrid.ItemsSource = Devices;
|
||||||
ConnectionMessageText.Text = "HRMS: Connection timed out. You can retry from Devices.";
|
ConnectionMessageText.Text = "HRMS: Connection timed out. You can retry from Devices.";
|
||||||
|
UpdateDevicesVisualState(isLoading: false);
|
||||||
AppLogger.Warning("[STARTUP] HRMS initialization timed out.");
|
AppLogger.Warning("[STARTUP] HRMS initialization timed out.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -58,6 +64,7 @@ public partial class MainWindow : Window
|
||||||
Devices = [];
|
Devices = [];
|
||||||
DevicesGrid.ItemsSource = Devices;
|
DevicesGrid.ItemsSource = Devices;
|
||||||
ConnectionMessageText.Text = "Unable to load HRMS devices. See the application log for details.";
|
ConnectionMessageText.Text = "Unable to load HRMS devices. See the application log for details.";
|
||||||
|
UpdateDevicesVisualState(isLoading: false);
|
||||||
}
|
}
|
||||||
await LoadDashboardAsync();
|
await LoadDashboardAsync();
|
||||||
AppLogger.Info("[STARTUP] HRMS initialization completed");
|
AppLogger.Info("[STARTUP] HRMS initialization completed");
|
||||||
|
|
@ -66,39 +73,19 @@ public partial class MainWindow : Window
|
||||||
|
|
||||||
private Task LoadDashboardAsync() => RefreshDashboardAsync(reloadDevices: false);
|
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;
|
var inFlight = _dashboardRefreshTask;
|
||||||
DashboardPage.Visibility = page == "Dashboard" ? Visibility.Visible : Visibility.Collapsed;
|
if (inFlight is { IsCompleted: false })
|
||||||
DevicesPage.Visibility = page == "Devices" ? Visibility.Visible : Visibility.Collapsed;
|
return inFlight;
|
||||||
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)
|
_dashboardRefreshTask = RefreshDashboardCoreAsync(reloadDevices);
|
||||||
PlaceholderTitle.Text = page;
|
return _dashboardRefreshTask;
|
||||||
|
|
||||||
_ = 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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshDashboardAsync(bool reloadDevices = false)
|
private async Task RefreshDashboardCoreAsync(bool reloadDevices)
|
||||||
{
|
{
|
||||||
if (!await _dashboardRefreshLock.WaitAsync(0).ConfigureAwait(true))
|
await _dashboardRefreshLock.WaitAsync().ConfigureAwait(true);
|
||||||
return;
|
|
||||||
|
|
||||||
var originalButtonContent = RefreshDashboardButton.Content;
|
var originalButtonContent = RefreshDashboardButton.Content;
|
||||||
try
|
try
|
||||||
|
|
@ -112,6 +99,7 @@ public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
Devices = await _deviceService.LoadUnifiedAsync().ConfigureAwait(true);
|
Devices = await _deviceService.LoadUnifiedAsync().ConfigureAwait(true);
|
||||||
DevicesGrid.ItemsSource = Devices;
|
DevicesGrid.ItemsSource = Devices;
|
||||||
|
UpdateDevicesVisualState(isLoading: false);
|
||||||
DashboardErrorText.Visibility = Visibility.Collapsed;
|
DashboardErrorText.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -138,8 +126,22 @@ public partial class MainWindow : Window
|
||||||
}
|
}
|
||||||
|
|
||||||
Dashboard.IsLoadingConnectivity = true;
|
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);
|
Dashboard.UpdateFromDevices(Devices);
|
||||||
|
|
||||||
var history = await _dashboardService.LoadRecentHistoryAsync().ConfigureAwait(true);
|
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.Text = "Unable to load device data.\nCheck the HRMS connection.";
|
||||||
DashboardErrorText.Visibility = Visibility.Visible;
|
DashboardErrorText.Visibility = Visibility.Visible;
|
||||||
Dashboard.IsInitialLoad = false;
|
Dashboard.IsInitialLoad = false;
|
||||||
Dashboard.IsLoadingConnectivity = false;
|
Dashboard.EndConnectivityRefresh();
|
||||||
Dashboard.UpdateFromDevices(Devices);
|
Dashboard.UpdateFromDevices(Devices);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Dashboard.IsLoadingConnectivity = false;
|
Dashboard.EndConnectivityRefresh();
|
||||||
|
Dashboard.UpdateFromDevices(Devices);
|
||||||
RefreshDashboardButton.IsEnabled = true;
|
RefreshDashboardButton.IsEnabled = true;
|
||||||
RefreshDashboardButton.Content = originalButtonContent;
|
RefreshDashboardButton.Content = originalButtonContent;
|
||||||
_dashboardRefreshLock.Release();
|
_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 void RefreshDashboard() => _ = LoadDashboardAsync();
|
||||||
|
|
||||||
private async void RefreshDashboard_Click(object sender, RoutedEventArgs e) =>
|
private async void RefreshDashboard_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
|
@ -182,8 +215,17 @@ public partial class MainWindow : Window
|
||||||
|
|
||||||
private async Task ReloadDevicesAsync()
|
private async Task ReloadDevicesAsync()
|
||||||
{
|
{
|
||||||
|
var originalContent = RefreshDevicesButton.Content;
|
||||||
try
|
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();
|
Devices = await _deviceService.LoadUnifiedAsync();
|
||||||
ConnectionMessageText.Text = "Loaded latest devices from HRMS and manual storage.";
|
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;
|
ConnectionMessageText.Text = "Unable to load HRMS devices: " + ex.Message;
|
||||||
}
|
}
|
||||||
DevicesGrid.ItemsSource = Devices;
|
finally
|
||||||
|
{
|
||||||
|
DevicesGrid.ItemsSource = Devices;
|
||||||
|
UpdateDevicesVisualState(isLoading: false);
|
||||||
|
RefreshDevicesButton.IsEnabled = true;
|
||||||
|
RefreshDevicesButton.Content = originalContent;
|
||||||
|
}
|
||||||
await LoadDashboardAsync();
|
await LoadDashboardAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDevice_Click(object sender, RoutedEventArgs e)
|
private void ClearDeviceEditor()
|
||||||
{
|
{
|
||||||
_editingDevice = null;
|
_editingDevice = null;
|
||||||
DeviceNameBox.Text = "";
|
DeviceNameBox.Text = "";
|
||||||
|
|
@ -205,6 +253,32 @@ public partial class MainWindow : Window
|
||||||
UsernameBox.Text = "";
|
UsernameBox.Text = "";
|
||||||
PasswordBox.Clear();
|
PasswordBox.Clear();
|
||||||
ModelBox.Text = "";
|
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;
|
DeviceEditor.Visibility = Visibility.Visible;
|
||||||
DeviceNameBox.Focus();
|
DeviceNameBox.Focus();
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +360,7 @@ public partial class MainWindow : Window
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _connectionService.TestAsync(SelectedDevice!, CancellationToken.None).ConfigureAwait(true);
|
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.Text = result.IsConnected ? "✓ " + result.Message : "✕ Connection failed\n" + result.Message;
|
||||||
ConnectionMessageText.Foreground = result.IsConnected
|
ConnectionMessageText.Foreground = result.IsConnected
|
||||||
? System.Windows.Media.Brushes.ForestGreen
|
? System.Windows.Media.Brushes.ForestGreen
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue