krona-staff/apps/KronaStaffTerminal/MainPage.xaml.cs
Ochenstarik efd5bd5c60
Some checks are pending
ci / verify (push) Waiting to run
feat: add SLK20R attendance terminal
2026-09-17 20:51:20 +07:00

233 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using KronaStaffTerminal.Models;
using KronaStaffTerminal.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace KronaStaffTerminal;
public sealed partial class MainPage : Page
{
private readonly TerminalStore _store = new();
private readonly NotificationService _notifications = new();
private readonly FingerprintScanner _scanner = new();
private TerminalState _state = new();
private bool _loaded;
public MainPage()
{
InitializeComponent();
_scanner.StatusChanged += message => Enqueue(() => SetStatus(message));
_scanner.RegistrationProgress += count => Enqueue(() => SetStatus($"Регистрация отпечатка: {count}/3", InfoBarSeverity.Informational));
_scanner.RegistrationCompleted += (id, template) => Enqueue(() => _ = CompleteRegistrationAsync(id, template));
_scanner.FingerRecognized += (id, score) => Enqueue(() => _ = RecordArrivalAsync(id, score));
}
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
if (_loaded) return;
_loaded = true;
_state = await _store.LoadAsync();
LoadSettings();
RefreshLists();
ConnectScanner();
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
_scanner.Dispose();
}
private void ConnectScanner()
{
try
{
_scanner.Connect(_state.Settings.ReaderIndex, _state.Employees
.Where(employee => employee.HasFingerprint)
.Select(employee => (employee.FingerprintId, employee.FingerprintTemplate!)));
ReaderCombo.Items.Clear();
for (var index = 0; index < _scanner.DeviceCount; index++)
{
ReaderCombo.Items.Add($"SLK20R №{index + 1}");
}
ReaderCombo.SelectedIndex = _scanner.ReaderIndex;
SetStatus($"Сканер SLK20R №{_scanner.ReaderIndex + 1} подключён. Зарегистрировано сотрудников: {_state.Employees.Count(employee => employee.HasFingerprint)}.", InfoBarSeverity.Success);
}
catch (Exception exception)
{
SetStatus(exception.Message, InfoBarSeverity.Error);
}
}
private async void AddEmployee_Click(object sender, RoutedEventArgs e)
{
var name = EmployeeNameBox.Text.Trim();
var number = PersonnelNumberBox.Text.Trim();
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(number))
{
SetStatus("Заполните имя и табельный номер сотрудника.", InfoBarSeverity.Warning);
return;
}
if (_state.Employees.Any(employee => employee.PersonnelNumber.Equals(number, StringComparison.OrdinalIgnoreCase)))
{
SetStatus("Сотрудник с таким табельным номером уже есть.", InfoBarSeverity.Warning);
return;
}
var employee = new EmployeeRecord { DisplayName = name, PersonnelNumber = number };
_state.Employees.Add(employee);
await _store.SaveAsync(_state);
EmployeeNameBox.Text = "";
PersonnelNumberBox.Text = "";
RefreshLists();
EmployeesList.SelectedItem = employee;
SetStatus($"{name} добавлен. Теперь зарегистрируйте отпечаток.", InfoBarSeverity.Success);
}
private void RegisterFingerprint_Click(object sender, RoutedEventArgs e)
{
if (EmployeesList.SelectedItem is not EmployeeRecord employee)
{
SetStatus("Сначала выберите сотрудника.", InfoBarSeverity.Warning);
return;
}
if (!_scanner.IsConnected)
{
SetStatus("Сканер не подключён.", InfoBarSeverity.Error);
return;
}
if (employee.FingerprintId <= 0)
{
employee.FingerprintId = _state.Employees.Select(item => item.FingerprintId).DefaultIfEmpty(0).Max() + 1;
}
_scanner.BeginRegistration(employee.FingerprintId);
}
private async Task CompleteRegistrationAsync(int fingerprintId, string template)
{
var employee = _state.Employees.FirstOrDefault(item => item.FingerprintId == fingerprintId);
if (employee is null) return;
employee.FingerprintTemplate = template;
await _store.SaveAsync(_state);
RefreshLists();
TerminalMessage.Text = $"Отпечаток сотрудника «{employee.DisplayName}» зарегистрирован";
SetStatus($"Отпечаток сотрудника «{employee.DisplayName}» зарегистрирован.", InfoBarSeverity.Success);
}
private async void DeleteEmployee_Click(object sender, RoutedEventArgs e)
{
if (EmployeesList.SelectedItem is not EmployeeRecord employee)
{
SetStatus("Сначала выберите сотрудника.", InfoBarSeverity.Warning);
return;
}
var dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = "Удалить сотрудника?",
Content = $"{employee.DisplayName} и зарегистрированный отпечаток будут удалены из терминала.",
PrimaryButtonText = "Удалить",
CloseButtonText = "Отмена",
DefaultButton = ContentDialogButton.Close
};
if (await dialog.ShowAsync() != ContentDialogResult.Primary) return;
_state.Employees.Remove(employee);
await _store.SaveAsync(_state);
RefreshLists();
ConnectScanner();
SetStatus("Сотрудник удалён.", InfoBarSeverity.Success);
}
private async Task RecordArrivalAsync(int fingerprintId, int score)
{
var employee = _state.Employees.FirstOrDefault(item => item.FingerprintId == fingerprintId);
if (employee is null) return;
var now = DateTimeOffset.UtcNow;
var alreadyRecorded = _state.Attendance.Any(item =>
item.EmployeeId == employee.Id && item.OccurredAtUtc.ToLocalTime().Date == now.ToLocalTime().Date);
if (alreadyRecorded)
{
TerminalMessage.Text = $"{employee.DisplayName}: приход сегодня уже отмечен";
SetStatus($"Повторное сканирование распознано, новая отметка не создана. Точность: {score}.", InfoBarSeverity.Informational);
return;
}
var record = new AttendanceRecord
{
EmployeeId = employee.Id,
EmployeeName = employee.DisplayName,
OccurredAtUtc = now,
ReaderIndex = _scanner.ReaderIndex
};
_state.Attendance.Insert(0, record);
await _store.SaveAsync(_state);
RefreshLists();
TerminalMessage.Text = $"Здравствуйте, {employee.DisplayName}!\nПриход отмечен в {now.ToLocalTime():HH:mm}";
SetStatus($"Приход отмечен. Точность распознавания: {score}.", InfoBarSeverity.Success);
var delivery = await _notifications.SendArrivalAsync(_state.Settings, employee.DisplayName, now.ToLocalTime());
record.NotificationDelivered = delivery.Delivered;
record.NotificationError = delivery.Error;
await _store.SaveAsync(_state);
RefreshLists();
if (delivery.Error is not null)
{
SetStatus($"Приход сохранён, но уведомление не отправлено: {delivery.Error}", InfoBarSeverity.Warning);
}
}
private async void SaveSettings_Click(object sender, RoutedEventArgs e)
{
CopySettingsFromControls();
await _store.SaveAsync(_state);
ConnectScanner();
SetStatus("Настройки сохранены.", InfoBarSeverity.Success);
}
private async void TestNotification_Click(object sender, RoutedEventArgs e)
{
CopySettingsFromControls();
var result = await _notifications.SendTestAsync(_state.Settings);
SetStatus(result.Delivered ? "Тестовое сообщение отправлено." : result.Error ?? "Включите Telegram или MAX и заполните реквизиты.", result.Delivered ? InfoBarSeverity.Success : InfoBarSeverity.Warning);
}
private void CopySettingsFromControls()
{
_state.Settings.ReaderIndex = Math.Max(0, ReaderCombo.SelectedIndex);
_state.Settings.TelegramEnabled = TelegramEnabledSwitch.IsOn;
_state.Settings.TelegramBotToken = TelegramTokenBox.Password.Trim();
_state.Settings.TelegramChatId = TelegramChatBox.Text.Trim();
_state.Settings.MaxEnabled = MaxEnabledSwitch.IsOn;
_state.Settings.MaxBotToken = MaxTokenBox.Password.Trim();
_state.Settings.MaxChatId = MaxChatBox.Text.Trim();
}
private void LoadSettings()
{
TelegramEnabledSwitch.IsOn = _state.Settings.TelegramEnabled;
TelegramTokenBox.Password = _state.Settings.TelegramBotToken;
TelegramChatBox.Text = _state.Settings.TelegramChatId;
MaxEnabledSwitch.IsOn = _state.Settings.MaxEnabled;
MaxTokenBox.Password = _state.Settings.MaxBotToken;
MaxChatBox.Text = _state.Settings.MaxChatId;
}
private void RefreshLists()
{
EmployeesList.ItemsSource = null;
EmployeesList.ItemsSource = _state.Employees.OrderBy(employee => employee.DisplayName).ToList();
AttendanceList.ItemsSource = null;
AttendanceList.ItemsSource = _state.Attendance.OrderByDescending(item => item.OccurredAtUtc).Take(100).ToList();
}
private void SetStatus(string message, InfoBarSeverity severity = InfoBarSeverity.Informational)
{
StatusBar.Message = message;
StatusBar.Severity = severity;
StatusBar.IsOpen = true;
}
private static void Enqueue(Action action) => App.DispatcherQueue.TryEnqueue(() => action());
}