233 lines
10 KiB
C#
233 lines
10 KiB
C#
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());
|
||
}
|