fix(terminal): handle locked scans consistently
This commit is contained in:
parent
0f530dc633
commit
057ec75b26
5 changed files with 315 additions and 25 deletions
|
|
@ -220,4 +220,168 @@ public class AttendanceTrackerTests
|
||||||
|
|
||||||
Assert.Empty(checkedOutRecords);
|
Assert.Empty(checkedOutRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Locked_RegularEmployee_FirstScan_RecordsAttendanceAndKeepsLocked()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.False(result.ShouldUnlock);
|
||||||
|
Assert.NotNull(result.AttendanceResult.Record);
|
||||||
|
Assert.Equal("Приход", result.AttendanceResult.Record.EventType);
|
||||||
|
Assert.Equal("Отметка прихода или ухода зафиксирована. Интерфейс остаётся заблокированным.", result.LockScreenMessage);
|
||||||
|
Assert.Single(state.Attendance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Locked_Administrator_FirstScan_RecordsAttendanceAndUnlocks()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
emp.IsAdministrator = true;
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.True(result.ShouldUnlock);
|
||||||
|
Assert.NotNull(result.AttendanceResult.Record);
|
||||||
|
Assert.Equal("Приход", result.AttendanceResult.Record.EventType);
|
||||||
|
Assert.Null(result.LockScreenMessage);
|
||||||
|
Assert.Contains(emp.DisplayName, result.StatusMessage);
|
||||||
|
Assert.Single(state.Attendance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Locked_Administrator_SecondScanAfterGuard_RecordsCheckOutAndUnlocks()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
emp.IsAdministrator = true;
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
// First scan: check-in
|
||||||
|
await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
// Second scan after 35 seconds (guard is 30s)
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now.AddSeconds(35), isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.True(result.ShouldUnlock);
|
||||||
|
Assert.NotNull(result.AttendanceResult.Record);
|
||||||
|
Assert.Equal("Уход", result.AttendanceResult.Record.EventType);
|
||||||
|
Assert.Null(result.LockScreenMessage);
|
||||||
|
Assert.Equal(2, state.Attendance.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Locked_RegularEmployee_DuplicateScan_RejectsDuplicateAndKeepsLocked()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
// First scan
|
||||||
|
await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
// Immediate repeat scan 5s later
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now.AddSeconds(5), isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.False(result.ShouldUnlock);
|
||||||
|
Assert.True(result.AttendanceResult.IsDuplicateSuppressed);
|
||||||
|
Assert.Equal(25, result.AttendanceResult.RemainingSeconds);
|
||||||
|
Assert.Equal("Повторный скан отклонён. Повторите через 25 сек.", result.LockScreenMessage);
|
||||||
|
Assert.Single(state.Attendance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Locked_Administrator_DuplicateScan_RejectsDuplicateAndUnlocks()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
emp.IsAdministrator = true;
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
// First scan
|
||||||
|
await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
// Immediate repeat scan 5s later
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now.AddSeconds(5), isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.True(result.ShouldUnlock);
|
||||||
|
Assert.True(result.AttendanceResult.IsDuplicateSuppressed);
|
||||||
|
Assert.Equal(25, result.AttendanceResult.RemainingSeconds);
|
||||||
|
Assert.Null(result.LockScreenMessage);
|
||||||
|
Assert.Single(state.Attendance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Locked_BlockedEmployee_DoesNotRecordAttendance()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
emp.IsBlocked = true;
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: true, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.False(result.ShouldUnlock);
|
||||||
|
Assert.False(result.AttendanceResult.Success);
|
||||||
|
Assert.Equal("Этот сотрудник заблокирован.", result.LockScreenMessage);
|
||||||
|
Assert.Empty(state.Attendance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Unlocked_RegularEmployee_RecordsAttendanceWithoutUnlock()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: false, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.False(result.ShouldUnlock);
|
||||||
|
Assert.NotNull(result.AttendanceResult.Record);
|
||||||
|
Assert.Equal("Приход", result.AttendanceResult.Record.EventType);
|
||||||
|
Assert.Null(result.LockScreenMessage);
|
||||||
|
Assert.Single(state.Attendance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessScan_Unlocked_Administrator_RecordsAttendanceWithoutUnlock()
|
||||||
|
{
|
||||||
|
var (state, emp) = CreateTestState();
|
||||||
|
emp.IsAdministrator = true;
|
||||||
|
var tracker = new AttendanceTracker();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
var result = await tracker.ProcessScanAsync(
|
||||||
|
state, emp, 0, now, isApplicationLocked: false, _ => Task.CompletedTask);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.False(result.ShouldUnlock);
|
||||||
|
Assert.NotNull(result.AttendanceResult.Record);
|
||||||
|
Assert.Equal("Приход", result.AttendanceResult.Record.EventType);
|
||||||
|
Assert.Null(result.LockScreenMessage);
|
||||||
|
Assert.Single(state.Attendance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,9 @@ public partial class App : Application
|
||||||
|
|
||||||
public void NotifyMinimizedToTray() => _trayService?.ShowRunningBalloon();
|
public void NotifyMinimizedToTray() => _trayService?.ShowRunningBalloon();
|
||||||
|
|
||||||
|
public void ShowNotificationBalloon(string title, string message, uint infoFlags = 1) =>
|
||||||
|
_trayService?.ShowNotificationBalloon(title, message, infoFlags);
|
||||||
|
|
||||||
public void UpdateTray(TrayState state, int pendingCount, string? statusText = null) =>
|
public void UpdateTray(TrayState state, int pendingCount, string? statusText = null) =>
|
||||||
_trayService?.UpdateState(state, pendingCount, statusText);
|
_trayService?.UpdateState(state, pendingCount, statusText);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.0" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.0" />
|
||||||
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools.WinApp" Version="0.6.1" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools.WinApp" Version="0.6.1" />
|
||||||
|
<PackageReference Include="System.Windows.Extensions" Version="10.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
@ -97,14 +98,8 @@
|
||||||
<_WinUiAsset Include="$(TargetDir)Assets\**\*" />
|
<_WinUiAsset Include="$(TargetDir)Assets\**\*" />
|
||||||
<_WinUiStyle Include="$(TargetDir)Styles\**\*" />
|
<_WinUiStyle Include="$(TargetDir)Styles\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Copy SourceFiles="@(_WinUiRootResource)"
|
<Copy SourceFiles="@(_WinUiRootResource)" DestinationFolder="$(PublishDir)" SkipUnchangedFiles="true" />
|
||||||
DestinationFolder="$(PublishDir)"
|
<Copy SourceFiles="@(_WinUiAsset)" DestinationFiles="@(_WinUiAsset->'$(PublishDir)Assets\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
||||||
SkipUnchangedFiles="true" />
|
<Copy SourceFiles="@(_WinUiStyle)" DestinationFiles="@(_WinUiStyle->'$(PublishDir)Styles\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
||||||
<Copy SourceFiles="@(_WinUiAsset)"
|
|
||||||
DestinationFiles="@(_WinUiAsset->'$(PublishDir)Assets\%(RecursiveDir)%(Filename)%(Extension)')"
|
|
||||||
SkipUnchangedFiles="true" />
|
|
||||||
<Copy SourceFiles="@(_WinUiStyle)"
|
|
||||||
DestinationFiles="@(_WinUiStyle->'$(PublishDir)Styles\%(RecursiveDir)%(Filename)%(Extension)')"
|
|
||||||
SkipUnchangedFiles="true" />
|
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -963,19 +963,7 @@ public sealed partial class MainPage : Page
|
||||||
else SetStatus("Сотрудник заблокирован. Отметка не создана.", InfoBarSeverity.Warning);
|
else SetStatus("Сотрудник заблокирован. Отметка не создана.", InfoBarSeverity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_applicationLocked)
|
|
||||||
{
|
|
||||||
if (!employee.IsAdministrator)
|
|
||||||
{
|
|
||||||
await RecordAttendanceAsync(employee, score);
|
|
||||||
LockScreenMessage.Text = "Отметка прихода или ухода зафиксирована. Интерфейс остаётся заблокированным.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_attendanceSuppressedFingerprintId = fingerprintId;
|
|
||||||
_attendanceSuppressedUntilUtc = DateTimeOffset.UtcNow.AddSeconds(3);
|
|
||||||
UnlockApplication($"employee:{employee.Id}", $"Вход выполнен по отпечатку администратора {employee.DisplayName}.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_adminAuthCompletion is not null)
|
if (_adminAuthCompletion is not null)
|
||||||
{
|
{
|
||||||
if (employee.IsAdministrator)
|
if (employee.IsAdministrator)
|
||||||
|
|
@ -1004,30 +992,57 @@ public sealed partial class MainPage : Page
|
||||||
}
|
}
|
||||||
_attendanceSuppressedFingerprintId = null;
|
_attendanceSuppressedFingerprintId = null;
|
||||||
_attendanceSuppressedUntilUtc = default;
|
_attendanceSuppressedUntilUtc = default;
|
||||||
|
|
||||||
|
if (_applicationLocked)
|
||||||
|
{
|
||||||
|
var scanResult = await RecordAttendanceAsync(employee, score);
|
||||||
|
if (scanResult.ShouldUnlock)
|
||||||
|
{
|
||||||
|
_attendanceSuppressedFingerprintId = fingerprintId;
|
||||||
|
_attendanceSuppressedUntilUtc = DateTimeOffset.UtcNow.AddSeconds(3);
|
||||||
|
UnlockApplication($"employee:{employee.Id}", scanResult.StatusMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LockScreenMessage.Text = scanResult.LockScreenMessage;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await RecordAttendanceAsync(employee, score);
|
await RecordAttendanceAsync(employee, score);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RecordAttendanceAsync(EmployeeRecord employee, int score)
|
private async Task<ScanRecognitionResult> RecordAttendanceAsync(EmployeeRecord employee, int score)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
var result = await _attendanceTracker.TryRecordAttendanceAsync(
|
var scanResult = await _attendanceTracker.ProcessScanAsync(
|
||||||
_state,
|
_state,
|
||||||
employee,
|
employee,
|
||||||
_scanner.ReaderIndex,
|
_scanner.ReaderIndex,
|
||||||
now,
|
now,
|
||||||
|
_applicationLocked,
|
||||||
state => _store.SaveAsync(state));
|
state => _store.SaveAsync(state));
|
||||||
|
|
||||||
|
var result = scanResult.AttendanceResult;
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
if (result.IsDuplicateSuppressed)
|
if (result.IsDuplicateSuppressed)
|
||||||
{
|
{
|
||||||
|
PlayDuplicateSoundSafe();
|
||||||
|
ShowTrayNotification(
|
||||||
|
"Krona Staff",
|
||||||
|
$"Повторный скан отклонён. Повторите через {result.RemainingSeconds} сек.");
|
||||||
TerminalMessage.Text = $"{TerminalDisplayName(employee)}\nПовторный скан отклонён\nПовторите через {result.RemainingSeconds} сек.";
|
TerminalMessage.Text = $"{TerminalDisplayName(employee)}\nПовторный скан отклонён\nПовторите через {result.RemainingSeconds} сек.";
|
||||||
SetStatus(result.Message, InfoBarSeverity.Informational);
|
SetStatus(result.Message, InfoBarSeverity.Informational);
|
||||||
}
|
}
|
||||||
return;
|
return scanResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var record = result.Record!;
|
var record = result.Record!;
|
||||||
|
PlaySuccessSoundSafe();
|
||||||
|
ShowTrayNotification(
|
||||||
|
"Krona Staff",
|
||||||
|
$"{TerminalDisplayName(employee)}: {record.EventType} в {now.ToLocalTime():HH:mm}");
|
||||||
|
|
||||||
RefreshLists();
|
RefreshLists();
|
||||||
TerminalMessage.Text = $"{TerminalDisplayName(employee)}\n{record.EventType} отмечен в {now.ToLocalTime():HH:mm}";
|
TerminalMessage.Text = $"{TerminalDisplayName(employee)}\n{record.EventType} отмечен в {now.ToLocalTime():HH:mm}";
|
||||||
SetStatus($"{record.EventType} отмечен. Точность распознавания: {score}.", InfoBarSeverity.Success);
|
SetStatus($"{record.EventType} отмечен. Точность распознавания: {score}.", InfoBarSeverity.Success);
|
||||||
|
|
@ -1046,6 +1061,47 @@ public sealed partial class MainPage : Page
|
||||||
{
|
{
|
||||||
SetStatus($"{record.EventType} сохранён, но уведомление не отправлено: {delivery.Error}", InfoBarSeverity.Warning);
|
SetStatus($"{record.EventType} сохранён, но уведомление не отправлено: {delivery.Error}", InfoBarSeverity.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return scanResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PlaySuccessSoundSafe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Media.SystemSounds.Asterisk.Play();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
CrashLogService.Write(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PlayDuplicateSoundSafe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Media.SystemSounds.Exclamation.Play();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
CrashLogService.Write(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ShowTrayNotification(string title, string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Application.Current is App app)
|
||||||
|
{
|
||||||
|
app.ShowNotificationBalloon(title, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
CrashLogService.Write(exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunAutomaticCheckoutIfDueAsync()
|
private async Task RunAutomaticCheckoutIfDueAsync()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ public sealed record AttendanceRecordResult(
|
||||||
bool IsDuplicateSuppressed,
|
bool IsDuplicateSuppressed,
|
||||||
int RemainingSeconds = 0);
|
int RemainingSeconds = 0);
|
||||||
|
|
||||||
|
public sealed record ScanRecognitionResult(
|
||||||
|
bool Success,
|
||||||
|
AttendanceRecordResult AttendanceResult,
|
||||||
|
bool ShouldUnlock,
|
||||||
|
string? LockScreenMessage,
|
||||||
|
string StatusMessage);
|
||||||
|
|
||||||
public sealed class AttendanceTracker
|
public sealed class AttendanceTracker
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||||
|
|
@ -77,6 +84,71 @@ public sealed class AttendanceTracker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ScanRecognitionResult> ProcessScanAsync(
|
||||||
|
TerminalState state,
|
||||||
|
EmployeeRecord employee,
|
||||||
|
int readerIndex,
|
||||||
|
DateTimeOffset nowUtc,
|
||||||
|
bool isApplicationLocked,
|
||||||
|
Func<TerminalState, Task> saveStateCallback)
|
||||||
|
{
|
||||||
|
if (employee.IsBlocked)
|
||||||
|
{
|
||||||
|
return new ScanRecognitionResult(
|
||||||
|
Success: false,
|
||||||
|
AttendanceResult: new AttendanceRecordResult(
|
||||||
|
Success: false,
|
||||||
|
Record: null,
|
||||||
|
Message: "Сотрудник заблокирован. Отметка не создана.",
|
||||||
|
IsDuplicateSuppressed: false),
|
||||||
|
ShouldUnlock: false,
|
||||||
|
LockScreenMessage: isApplicationLocked ? "Этот сотрудник заблокирован." : null,
|
||||||
|
StatusMessage: "Сотрудник заблокирован. Отметка не создана.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var attendanceResult = await TryRecordAttendanceAsync(
|
||||||
|
state,
|
||||||
|
employee,
|
||||||
|
readerIndex,
|
||||||
|
nowUtc,
|
||||||
|
saveStateCallback);
|
||||||
|
|
||||||
|
if (isApplicationLocked)
|
||||||
|
{
|
||||||
|
if (!employee.IsAdministrator)
|
||||||
|
{
|
||||||
|
var lockMessage = attendanceResult.Success
|
||||||
|
? "Отметка прихода или ухода зафиксирована. Интерфейс остаётся заблокированным."
|
||||||
|
: attendanceResult.IsDuplicateSuppressed
|
||||||
|
? $"Повторный скан отклонён. Повторите через {attendanceResult.RemainingSeconds} сек."
|
||||||
|
: "Отметка не создана. Интерфейс остаётся заблокированным.";
|
||||||
|
|
||||||
|
return new ScanRecognitionResult(
|
||||||
|
Success: attendanceResult.Success,
|
||||||
|
AttendanceResult: attendanceResult,
|
||||||
|
ShouldUnlock: false,
|
||||||
|
LockScreenMessage: lockMessage,
|
||||||
|
StatusMessage: attendanceResult.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Administrator scan in locked state:
|
||||||
|
// Attendance is recorded AND the interface should be unlocked
|
||||||
|
return new ScanRecognitionResult(
|
||||||
|
Success: attendanceResult.Success,
|
||||||
|
AttendanceResult: attendanceResult,
|
||||||
|
ShouldUnlock: true,
|
||||||
|
LockScreenMessage: null,
|
||||||
|
StatusMessage: $"Вход выполнен по отпечатку администратора {employee.DisplayName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ScanRecognitionResult(
|
||||||
|
Success: attendanceResult.Success,
|
||||||
|
AttendanceResult: attendanceResult,
|
||||||
|
ShouldUnlock: false,
|
||||||
|
LockScreenMessage: null,
|
||||||
|
StatusMessage: attendanceResult.Message);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<AttendanceRecord>> RunScheduledCheckoutAsync(
|
public async Task<List<AttendanceRecord>> RunScheduledCheckoutAsync(
|
||||||
TerminalState state,
|
TerminalState state,
|
||||||
DateTimeOffset nowUtc,
|
DateTimeOffset nowUtc,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue