using System.Text.Json.Serialization; namespace KronaStaffTerminal.Models; public sealed class EmployeeRecord { public string Id { get; set; } = UlidGenerator.New(); public string PersonnelNumber { get; set; } = ""; public string LastName { get; set; } = ""; public string FirstName { get; set; } = ""; public string Patronymic { get; set; } = ""; private string _displayName = ""; public string DisplayName { get { if (!string.IsNullOrWhiteSpace(_displayName)) return _displayName; var parts = new[] { LastName, FirstName, Patronymic }.Where(p => !string.IsNullOrWhiteSpace(p)); var combined = string.Join(" ", parts); return string.IsNullOrWhiteSpace(combined) ? "" : combined; } set => _displayName = value; } public string Position { get; set; } = ""; public string Gender { get; set; } = ""; public bool IsAdministrator { get; set; } public bool IsBlocked { get; set; } public DateTimeOffset? BlockedAtUtc { get; set; } public List Fingerprints { get; set; } = []; public string TelegramChatId { get; set; } = ""; public string VkPeerId { get; set; } = ""; public string MaxUserId { get; set; } = ""; public void SetFullName(string lastName, string firstName, string patronymic) { LastName = lastName?.Trim() ?? ""; FirstName = firstName?.Trim() ?? ""; Patronymic = patronymic?.Trim() ?? ""; var parts = new[] { LastName, FirstName, Patronymic }.Where(p => !string.IsNullOrWhiteSpace(p)); _displayName = string.Join(" ", parts); } // Legacy single-fingerprint fields are kept for one-way migration of existing encrypted state. public int FingerprintId { get; set; } public string? FingerprintTemplate { get; set; } [JsonIgnore] public bool HasFingerprint => Fingerprints.Count > 0; [JsonIgnore] public string FingerprintStatus => Fingerprints.Count switch { 0 => "Отпечатков нет", 1 => "1 отпечаток", 2 or 3 or 4 => $"{Fingerprints.Count} отпечатка", _ => $"{Fingerprints.Count} отпечатков" }; [JsonIgnore] public string AccessStatus => IsBlocked ? "Заблокирован" : IsAdministrator ? "Администратор" : "Сотрудник"; [JsonIgnore] public string NotificationStatus { get { if (IsBlocked) return "Отключены"; var channels = new List(); if (!string.IsNullOrWhiteSpace(TelegramChatId)) channels.Add("Telegram"); if (!string.IsNullOrWhiteSpace(VkPeerId)) channels.Add("VK"); if (!string.IsNullOrWhiteSpace(MaxUserId)) channels.Add("MAX"); return channels.Count == 0 ? "Не подключены" : string.Join(", ", channels); } } [JsonIgnore] public string GenderDisplay => string.IsNullOrWhiteSpace(Gender) ? "—" : Gender; } public sealed class FingerprintRecord { public int Id { get; set; } public string Template { get; set; } = ""; public string Label { get; set; } = ""; public DateTimeOffset EnrolledAtUtc { get; set; } = DateTimeOffset.UtcNow; } public sealed class AttendanceRecord { public string Id { get; set; } = UlidGenerator.New(); public string EmployeeId { get; set; } = ""; public string EmployeeName { get; set; } = ""; public DateTimeOffset OccurredAtUtc { get; set; } public int ReaderIndex { get; set; } public string EventType { get; set; } = "Приход"; public bool IsAutomatic { get; set; } public bool NotificationDelivered { get; set; } public string? NotificationError { get; set; } public bool ServerSynced { get; set; } public string? ServerSyncError { get; set; } [JsonIgnore] public string LocalTime => OccurredAtUtc.ToLocalTime().ToString("dd.MM.yyyy HH:mm:ss"); [JsonIgnore] public string DeliveryStatus => NotificationDelivered ? "Отправлено" : string.IsNullOrWhiteSpace(NotificationError) ? "Не настроено" : "Ошибка отправки"; } public sealed class TerminalSettings { public int ReaderIndex { get; set; } public bool StartWithWindows { get; set; } public string AdminPinSalt { get; set; } = ""; public string AdminPinHash { get; set; } = ""; public int AdminPinIterations { get; set; } = 120_000; public int FailedAdminPinAttempts { get; set; } public DateTimeOffset AdminPinBlockedUntilUtc { get; set; } public int DuplicateScanGuardSeconds { get; set; } = 120; public int MaximumShiftHours { get; set; } = 20; public bool AutomaticCheckoutEnabled { get; set; } public int AutomaticCheckoutHour { get; set; } = 2; public int AutomaticCheckoutMinute { get; set; } public DateTimeOffset LastAutomaticCheckoutUtc { get; set; } public bool ShowEmployeeNamesOnTerminal { get; set; } public bool TelegramEnabled { get; set; } public string TelegramBotToken { get; set; } = ""; // Legacy global chat IDs are retained only so existing encrypted state remains readable. public string TelegramChatId { get; set; } = ""; public bool VkEnabled { get; set; } public string VkGroupToken { get; set; } = ""; public bool MaxEnabled { get; set; } public string MaxBotToken { get; set; } = ""; public string MaxChatId { get; set; } = ""; public bool UpdateChecksEnabled { get; set; } = true; public string UpdateFeedUrl { get; set; } = "https://git.ochenstarik.ru/api/v1/repos/ochenstarik/krona-staff/releases/latest"; public string UpdateAccessToken { get; set; } = ""; public string UpdateTrustedSignerThumbprint { get; set; } = ""; public string StaffServerUrl { get; set; } = "https://192.168.1.81:8453"; public string StaffServerToken { get; set; } = ""; public string StaffServerCertificateSha256 { get; set; } = ""; public string TerminalDeviceId { get; set; } = UlidGenerator.New(); public DateTimeOffset LastUpdateCheckUtc { get; set; } } public sealed class TerminalState { public List Employees { get; set; } = []; public List Attendance { get; set; } = []; public List SecurityAudit { get; set; } = []; public TerminalSettings Settings { get; set; } = new(); } public sealed class TerminalAuditRecord { public string Id { get; set; } = UlidGenerator.New(); public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow; public string Action { get; set; } = ""; public string Actor { get; set; } = ""; public string Outcome { get; set; } = ""; public string Detail { get; set; } = ""; }