diff --git a/BusinessLogging.cs b/BusinessLogging.cs index bd26917..e7b822d 100644 --- a/BusinessLogging.cs +++ b/BusinessLogging.cs @@ -54,6 +54,7 @@ internal enum BizChannel Attendance, UserSync, Template, + DepartmentalSync, Unreachable } diff --git a/DeviceSettings.config b/DeviceSettings.config index 9cf6d02..b7a9bf3 100644 --- a/DeviceSettings.config +++ b/DeviceSettings.config @@ -7,7 +7,7 @@ "EnableTemplateDeviceToDbSync": false, "EnableTemplateDbToDeviceSync": false, -"SourceMachineIp": "", +"SourceMachineIp": "192.168.91.80", "TargetMachineIps": [ "192.168.91.80" ], @@ -15,11 +15,10 @@ "SyncEmployeeIds": [], "SyncDepartmentIds": [], -"EnableInitialDepartmentSync": true, -"InitialSyncDepartmentIds": [ -"357" -], +"EnableInitialDepartmentSync": false, +"InitialSyncDepartmentIds": [], "InitialSyncEmployeeIds": [], +"InitialSyncLocationSiteId": 2, "EnableEmployeePhotoSource": true, "EmployeePhotoBaseUrl": "https://portal.utopiaindustries.pk/uind/employee-photo/" } diff --git a/HikvisionAttendanceManager.InitialSync.cs b/HikvisionAttendanceManager.InitialSync.cs index ded0f14..515933a 100644 --- a/HikvisionAttendanceManager.InitialSync.cs +++ b/HikvisionAttendanceManager.InitialSync.cs @@ -27,21 +27,39 @@ internal sealed partial class HikvisionAttendanceManager ct.ThrowIfCancellationRequested(); + if (!_config.InitialSyncLocationSiteId.HasValue || + _config.InitialSyncLocationSiteId.Value <= 0) + { + const string reason = + "InitialSyncLocationSiteId is not configured; Initial Department Sync requires an explicit location-site filter."; + _logger.OpsWarn("INITIAL_SYNC", "skipped — " + reason); + _logger.Biz(BizChannel.DepartmentalSync, + "DEPARTMENTAL SYNC FAILED", + "", + "Reason : " + reason, + ""); + _logger.BizSeparator(BizChannel.DepartmentalSync); + return; + } + var locationSiteId = _config.InitialSyncLocationSiteId.Value; + var deptIds = (_config.InitialSyncDepartmentIds ?? new List()) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); - if (deptIds.Count == 0) - { - _logger.OpsWarn("INITIAL_SYNC", "skipped — InitialSyncDepartmentIds is empty"); - return; - } + var allActiveDepartmentsMode = deptIds.Count == 0; var targets = ResolveInitialSyncTargets(); if (targets.Count == 0) { _logger.OpsWarn("INITIAL_SYNC", "skipped — no valid TargetMachineIps"); + _logger.Biz(BizChannel.DepartmentalSync, + "DEPARTMENTAL SYNC FAILED", + "", + "Reason : No valid target machines are configured.", + ""); + _logger.BizSeparator(BizChannel.DepartmentalSync); return; } @@ -51,22 +69,33 @@ internal sealed partial class HikvisionAttendanceManager .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); - var employees = LoadEmployeesForInitialDepartmentSync(deptIds, employeeIdFilter, out var loadErr); + if (allActiveDepartmentsMode && employeeIdFilter.Count > 0) + { + _logger.OpsWarn("INITIAL_SYNC", + "InitialSyncEmployeeIds ignored because InitialSyncDepartmentIds is empty; " + + "mode=ALL_ACTIVE_DEPARTMENTS_AND_EMPLOYEES"); + employeeIdFilter.Clear(); + } + + var selection = LoadEmployeesForInitialDepartmentSync( + deptIds, + employeeIdFilter, + locationSiteId, + out var loadErr); if (!string.IsNullOrWhiteSpace(loadErr)) + { _logger.Warn("INITIAL_SYNC: employee lookup failed err=" + loadErr); - - if (employeeIdFilter.Count > 0) - _logger.Ops("INITIAL_SYNC", "Employee filter: departmentIds=" + string.Join(",", deptIds) + - " allowListCount=" + employeeIdFilter.Count + - " allowList=" + string.Join(",", employeeIdFilter) + - " matched=" + employees.Count); - - _logger.Ops("INITIAL_SYNC", "departmentIds=" + string.Join(",", deptIds) + - " employeeFilter=" + (employeeIdFilter.Count > 0 ? "selective(" + employeeIdFilter.Count + ")" : "whole-department") + - " employeesFound=" + employees.Count); - - if (employees.Count == 0) + _logger.Biz(BizChannel.DepartmentalSync, + "DEPARTMENTAL SYNC FAILED", + "", + "Reason : " + loadErr, + ""); + _logger.BizSeparator(BizChannel.DepartmentalSync); return; + } + + LogInitialDepartmentSelection(deptIds, employeeIdFilter, selection); + var employees = selection.Employees; var maxRetries = Math.Max(1, SyncPol.HttpMaxRetries); var photoBase = (_config.EmployeePhotoBaseUrl ?? "").Trim(); @@ -90,6 +119,12 @@ internal sealed partial class HikvisionAttendanceManager "Machine is not connected.", ""); _logger.BizSeparator(BizChannel.UserSync); + WriteDepartmentalSyncSummary( + target, + selection, + employeeIdFilter, + false, + Array.Empty()); continue; } @@ -97,9 +132,22 @@ internal sealed partial class HikvisionAttendanceManager "MACHINE " + (target.DeviceId ?? "") + " -> Total Users : " + employees.Count, ""); + if (employees.Count == 0) + { + WriteDepartmentalSyncSummary( + target, + selection, + employeeIdFilter, + true, + Array.Empty()); + _logger.BizSeparator(BizChannel.UserSync); + continue; + } + var existing = FetchAllUsersIsapiForSync(target, maxRetries, ct); var existingNos = new HashSet(existing.Select(u => u.EmployeeNo), StringComparer.OrdinalIgnoreCase); var faceRejectedEmployees = new List<(string EmployeeNo, string Reason)>(); + var employeeOutcomes = new List(); foreach (var emp in employees) { @@ -186,6 +234,15 @@ internal sealed partial class HikvisionAttendanceManager } finally { + employeeOutcomes.Add(new InitialSyncEmployeeOutcome + { + DepartmentId = emp.DepartmentId, + EmployeeNo = employeeNo, + Synced = faceUploaded, + Reason = faceUploaded + ? "" + : (string.IsNullOrWhiteSpace(reason) ? "face_upload_failed" : reason) + }); WriteInitialSyncEmployeeBizLog(target, employeeNo, userCreated, faceUploaded, reason); if (photoDownloaded && faceUploaded) @@ -222,6 +279,7 @@ internal sealed partial class HikvisionAttendanceManager } WriteFaceRejectedEmployeeSummary("INITIAL_SYNC", target, faceRejectedEmployees); + WriteDepartmentalSyncSummary(target, selection, employeeIdFilter, true, employeeOutcomes); _logger.BizSeparator(BizChannel.UserSync); } } @@ -356,15 +414,14 @@ internal sealed partial class HikvisionAttendanceManager return targets; } - private List LoadEmployeesForInitialDepartmentSync( + private InitialSyncSelection LoadEmployeesForInitialDepartmentSync( IReadOnlyList departmentIds, IReadOnlyList employeeIdAllowList, + int locationSiteId, out string error) { error = ""; - var result = new List(); - if (departmentIds == null || departmentIds.Count == 0) - return result; + var result = new InitialSyncSelection { LocationSiteId = locationSiteId }; if (!_config.EnableDbIntegration || _dbConnectionFactory == null) { error = "DB integration not available for initial department sync."; @@ -374,54 +431,131 @@ internal sealed partial class HikvisionAttendanceManager if (!_dbConnectionFactory.TryBuildConnectionString(out var cs, out error)) return result; - var hasAllowList = employeeIdAllowList != null && employeeIdAllowList.Count > 0; + var hasDepartmentFilter = departmentIds != null && departmentIds.Count > 0; + var hasAllowList = hasDepartmentFilter && + employeeIdAllowList != null && + employeeIdAllowList.Count > 0; try { using var conn = new MySqlConnection(cs); conn.Open(); - var sql = new StringBuilder("SELECT id, serial_number FROM employee WHERE department_id IN ("); - for (int i = 0; i < departmentIds.Count; i++) - { - if (i > 0) sql.Append(','); - sql.Append("@d").Append(i); - } - sql.Append(')'); - if (hasAllowList) + if (hasDepartmentFilter) { - sql.Append(" AND serial_number IN ("); - for (int i = 0; i < employeeIdAllowList.Count; i++) + var departmentSql = new StringBuilder( + "SELECT id, is_active FROM department WHERE id IN ("); + AppendSqlParameters(departmentSql, "@d", departmentIds.Count); + departmentSql.Append(')'); + + using var departmentCmd = new MySqlCommand(departmentSql.ToString(), conn); + AddSqlParameters(departmentCmd, "@d", departmentIds); + var foundDepartmentIds = new HashSet(StringComparer.OrdinalIgnoreCase); + using (var departmentReader = departmentCmd.ExecuteReader()) { - if (i > 0) sql.Append(','); - sql.Append("@e").Append(i); + while (departmentReader.Read()) + { + var departmentId = departmentReader["id"]?.ToString()?.Trim() ?? ""; + if (departmentId.Length == 0) + continue; + + foundDepartmentIds.Add(departmentId); + if (IsDatabaseActive(departmentReader["is_active"])) + result.ActiveDepartmentIds.Add(departmentId); + else + result.InactiveDepartmentIds.Add(departmentId); + } } - sql.Append(')'); + + foreach (var requestedId in departmentIds) + { + if (!foundDepartmentIds.Contains(requestedId)) + result.MissingDepartmentIds.Add(requestedId); + } + } + else + { + using var departmentCmd = new MySqlCommand( + "SELECT id FROM department WHERE is_active = 1", conn); + using (var departmentReader = departmentCmd.ExecuteReader()) + { + while (departmentReader.Read()) + { + var departmentId = departmentReader["id"]?.ToString()?.Trim() ?? ""; + if (departmentId.Length > 0) + result.ActiveDepartmentIds.Add(departmentId); + } + } + + using var inactiveDepartmentCmd = new MySqlCommand( + "SELECT COUNT(*) FROM department WHERE is_active IS NULL OR is_active <> 1", conn); + result.InactiveDepartmentCount = Convert.ToInt32(inactiveDepartmentCmd.ExecuteScalar() ?? 0); } - using var cmd = new MySqlCommand(sql.ToString(), conn); - for (int i = 0; i < departmentIds.Count; i++) - cmd.Parameters.AddWithValue("@d" + i, departmentIds[i]); + if (result.ActiveDepartmentIds.Count == 0) + return result; + + var employeeSql = new StringBuilder( + "SELECT e.id, e.serial_number, e.department_id " + + "FROM employee e " + + "INNER JOIN department d ON d.id = e.department_id " + + "WHERE d.is_active = 1 AND e.is_active = 1 " + + "AND e.location_site_id = @locationSiteId AND e.department_id IN ("); + AppendSqlParameters(employeeSql, "@activeDept", result.ActiveDepartmentIds.Count); + employeeSql.Append(')'); if (hasAllowList) { - for (int i = 0; i < employeeIdAllowList.Count; i++) - cmd.Parameters.AddWithValue("@e" + i, employeeIdAllowList[i]); + employeeSql.Append(" AND e.serial_number IN ("); + AppendSqlParameters(employeeSql, "@e", employeeIdAllowList.Count); + employeeSql.Append(')'); } - using var rd = cmd.ExecuteReader(); + using var cmd = new MySqlCommand(employeeSql.ToString(), conn); + cmd.Parameters.AddWithValue("@locationSiteId", locationSiteId); + AddSqlParameters(cmd, "@activeDept", result.ActiveDepartmentIds); + if (hasAllowList) + AddSqlParameters(cmd, "@e", employeeIdAllowList); + var seenSerial = new HashSet(StringComparer.OrdinalIgnoreCase); - while (rd.Read()) + using (var rd = cmd.ExecuteReader()) { - var serial = rd["serial_number"]?.ToString()?.Trim() ?? ""; - if (serial.Length == 0 || !seenSerial.Add(serial)) - continue; - result.Add(new InitialSyncEmployee + while (rd.Read()) { - Id = rd["id"]?.ToString()?.Trim() ?? "", - SerialNumber = serial, - Name = "" - }); + var serial = rd["serial_number"]?.ToString()?.Trim() ?? ""; + if (serial.Length == 0 || !seenSerial.Add(serial)) + continue; + result.Employees.Add(new InitialSyncEmployee + { + Id = rd["id"]?.ToString()?.Trim() ?? "", + SerialNumber = serial, + DepartmentId = rd["department_id"]?.ToString()?.Trim() ?? "", + Name = "" + }); + } } + + var inactiveEmployeeSql = new StringBuilder( + "SELECT COUNT(*) " + + "FROM employee e " + + "INNER JOIN department d ON d.id = e.department_id " + + "WHERE d.is_active = 1 AND (e.is_active IS NULL OR e.is_active <> 1) " + + "AND e.location_site_id = @inactiveLocationSiteId AND e.department_id IN ("); + AppendSqlParameters(inactiveEmployeeSql, "@inactiveDept", result.ActiveDepartmentIds.Count); + inactiveEmployeeSql.Append(')'); + if (hasAllowList) + { + inactiveEmployeeSql.Append(" AND e.serial_number IN ("); + AppendSqlParameters(inactiveEmployeeSql, "@inactiveEmp", employeeIdAllowList.Count); + inactiveEmployeeSql.Append(')'); + } + + using var inactiveEmployeeCmd = new MySqlCommand(inactiveEmployeeSql.ToString(), conn); + inactiveEmployeeCmd.Parameters.AddWithValue("@inactiveLocationSiteId", locationSiteId); + AddSqlParameters(inactiveEmployeeCmd, "@inactiveDept", result.ActiveDepartmentIds); + if (hasAllowList) + AddSqlParameters(inactiveEmployeeCmd, "@inactiveEmp", employeeIdAllowList); + result.SkippedInactiveEmployeeCount = + Convert.ToInt32(inactiveEmployeeCmd.ExecuteScalar() ?? 0); } catch (Exception ex) { @@ -431,6 +565,212 @@ internal sealed partial class HikvisionAttendanceManager return result; } + private void LogInitialDepartmentSelection( + IReadOnlyList requestedDepartmentIds, + IReadOnlyList employeeIdAllowList, + InitialSyncSelection selection) + { + var allDepartments = requestedDepartmentIds == null || requestedDepartmentIds.Count == 0; + var selectiveEmployees = !allDepartments && + employeeIdAllowList != null && + employeeIdAllowList.Count > 0; + var inactiveCount = allDepartments + ? selection.InactiveDepartmentCount + : selection.InactiveDepartmentIds.Count; + + _logger.Ops("INITIAL_SYNC", + "InitialSyncLocationSiteId=" + selection.LocationSiteId + + " departmentMode=" + (allDepartments ? "ALL_ACTIVE" : "SELECTED") + + " selectedDepartmentCount=" + (allDepartments + ? selection.ActiveDepartmentIds.Count + : requestedDepartmentIds.Count) + + " activeDepartmentCount=" + selection.ActiveDepartmentIds.Count + + " inactiveDepartmentCount=" + inactiveCount + + " activeDepartments=" + FormatLogValues(selection.ActiveDepartmentIds) + + " inactiveDepartments=" + (allDepartments + ? "(all inactive departments excluded)" + : FormatLogValues(selection.InactiveDepartmentIds)) + + " missingDepartments=" + FormatLogValues(selection.MissingDepartmentIds)); + + _logger.Ops("INITIAL_SYNC", + "employeeFilterMode=" + (selectiveEmployees ? "SELECTIVE" : "ALL") + + (selectiveEmployees + ? " allowListCount=" + employeeIdAllowList.Count + + " allowList=" + FormatLogValues(employeeIdAllowList) + : "") + + " finalEmployeeCount=" + selection.Employees.Count + + " skippedInactiveEmployees=" + selection.SkippedInactiveEmployeeCount); + } + + private void WriteDepartmentalSyncSummary( + HikvisionAttendanceWindowsService.DeviceConfig target, + InitialSyncSelection selection, + IReadOnlyList employeeIdAllowList, + bool targetOnline, + IReadOnlyList outcomes) + { + var lines = new List + { + "DEPARTMENTAL SYNC SUMMARY", + "", + "Machine ID : " + (target.DeviceId ?? ""), + "Machine IP : " + (target.Ip ?? ""), + "InitialSyncLocationSiteId : " + selection.LocationSiteId, + "Employee Filter : " + (employeeIdAllowList.Count > 0 ? "SELECTIVE" : "ALL"), + "Active Departments : " + selection.ActiveDepartmentIds.Count, + "Inactive Departments Skipped : " + + (selection.InactiveDepartmentIds.Count + selection.InactiveDepartmentCount), + "Eligible Active Employees : " + selection.Employees.Count, + "Skipped Inactive Employees : " + selection.SkippedInactiveEmployeeCount, + "" + }; + + foreach (var departmentId in selection.ActiveDepartmentIds + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + { + var eligibleCount = selection.Employees.Count(x => + string.Equals(x.DepartmentId, departmentId, StringComparison.OrdinalIgnoreCase)); + var departmentOutcomes = outcomes.Where(x => + string.Equals(x.DepartmentId, departmentId, StringComparison.OrdinalIgnoreCase)).ToList(); + var syncedCount = departmentOutcomes.Count(x => x.Synced); + var failedCount = Math.Max(0, eligibleCount - syncedCount); + + string status; + string reason = ""; + if (!targetOnline) + { + status = "NOT SYNCED"; + reason = "Target machine is offline."; + } + else if (eligibleCount == 0) + { + status = "NOT SYNCED"; + reason = employeeIdAllowList.Count > 0 + ? "No selected active employees belong to this active department." + : "Department has no active employees."; + } + else if (syncedCount == eligibleCount) + { + status = "SYNCED"; + } + else if (syncedCount > 0) + { + status = "PARTIALLY SYNCED"; + } + else + { + status = "NOT SYNCED"; + } + + lines.Add("Department " + departmentId + " : " + status); + lines.Add("Eligible Users : " + eligibleCount); + lines.Add("Synced Users : " + syncedCount); + lines.Add("Failed Users : " + failedCount); + if (failedCount > 0) + lines.Add("Failed Employee Numbers : " + + string.Join(",", targetOnline + ? departmentOutcomes.Where(x => !x.Synced).Select(x => x.EmployeeNo) + : selection.Employees + .Where(x => string.Equals( + x.DepartmentId, + departmentId, + StringComparison.OrdinalIgnoreCase)) + .Select(x => x.SerialNumber))); + if (!string.IsNullOrWhiteSpace(reason)) + lines.Add("Reason : " + reason); + + foreach (var failureGroup in departmentOutcomes + .Where(x => !x.Synced) + .GroupBy(x => DescribeInitialSyncFailure(x.Reason)) + .OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) + { + lines.Add("Failure Reason : " + failureGroup.Key + + " (" + failureGroup.Count() + " user(s))"); + } + + lines.Add(""); + } + + foreach (var departmentId in selection.InactiveDepartmentIds + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + { + lines.Add("Department " + departmentId + " : NOT SYNCED"); + lines.Add("Eligible Users : 0"); + lines.Add("Synced Users : 0"); + lines.Add("Failed Users : 0"); + lines.Add("Reason : Department is inactive."); + lines.Add(""); + } + + foreach (var departmentId in selection.MissingDepartmentIds + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + { + lines.Add("Department " + departmentId + " : NOT SYNCED"); + lines.Add("Eligible Users : 0"); + lines.Add("Synced Users : 0"); + lines.Add("Failed Users : 0"); + lines.Add("Reason : Department was not found."); + lines.Add(""); + } + + if (selection.ActiveDepartmentIds.Count == 0 && + selection.InactiveDepartmentIds.Count == 0 && + selection.MissingDepartmentIds.Count == 0) + { + lines.Add("No active departments were available for synchronization."); + lines.Add(""); + } + + var totalSynced = outcomes.Count(x => x.Synced); + lines.Add("TOTAL SYNCED USERS : " + totalSynced); + lines.Add("TOTAL FAILED USERS : " + Math.Max(0, selection.Employees.Count - totalSynced)); + lines.Add(""); + + _logger.Biz(BizChannel.DepartmentalSync, lines.ToArray()); + _logger.BizSeparator(BizChannel.DepartmentalSync); + } + + private static void AppendSqlParameters(StringBuilder sql, string prefix, int count) + { + for (int i = 0; i < count; i++) + { + if (i > 0) + sql.Append(','); + sql.Append(prefix).Append(i); + } + } + + private static void AddSqlParameters( + MySqlCommand command, + string prefix, + IReadOnlyList values) + { + for (int i = 0; i < values.Count; i++) + command.Parameters.AddWithValue(prefix + i, values[i]); + } + + private static bool IsDatabaseActive(object value) + { + if (value == null || value == DBNull.Value) + return false; + + if (value is bool boolValue) + return boolValue; + + return string.Equals( + Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)?.Trim(), + "1", + StringComparison.Ordinal); + } + + private static string FormatLogValues(IEnumerable values) + { + var items = (values ?? Enumerable.Empty()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToList(); + return items.Count == 0 ? "(none)" : string.Join(",", items); + } + private static bool TryDownloadEmployeePortalPhoto(string photoUrl, out byte[] bytes, out string error) { bytes = Array.Empty(); @@ -489,6 +829,26 @@ internal sealed partial class HikvisionAttendanceManager { public string Id { get; set; } = ""; public string SerialNumber { get; set; } = ""; + public string DepartmentId { get; set; } = ""; public string Name { get; set; } = ""; } + + private sealed class InitialSyncEmployeeOutcome + { + public string DepartmentId { get; set; } = ""; + public string EmployeeNo { get; set; } = ""; + public bool Synced { get; set; } + public string Reason { get; set; } = ""; + } + + private sealed class InitialSyncSelection + { + public int LocationSiteId { get; set; } + public List Employees { get; } = new List(); + public List ActiveDepartmentIds { get; } = new List(); + public List InactiveDepartmentIds { get; } = new List(); + public List MissingDepartmentIds { get; } = new List(); + public int InactiveDepartmentCount { get; set; } + public int SkippedInactiveEmployeeCount { get; set; } + } } diff --git a/HikvisionAttendanceWindowsService.cs b/HikvisionAttendanceWindowsService.cs index 527e1ca..db039dc 100644 --- a/HikvisionAttendanceWindowsService.cs +++ b/HikvisionAttendanceWindowsService.cs @@ -120,6 +120,13 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase [DataMember] public List InitialSyncEmployeeIds { get; set; } = new List(); + /// + /// Required HRMS employee.location_site_id filter for Initial Department Sync. + /// Null or a non-positive value prevents the initial sync from running. + /// + [DataMember(EmitDefaultValue = false)] + public int? InitialSyncLocationSiteId { get; set; } + /// When true, initial sync downloads JPEG photos from . [DataMember] public bool EnableEmployeePhotoSource { get; set; } @@ -582,6 +589,8 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase cfg.InitialSyncDepartmentIds = new List(overlay.InitialSyncDepartmentIds); if (overlay.InitialSyncEmployeeIds != null) cfg.InitialSyncEmployeeIds = new List(overlay.InitialSyncEmployeeIds); + if (overlay.InitialSyncLocationSiteId.HasValue) + cfg.InitialSyncLocationSiteId = overlay.InitialSyncLocationSiteId; cfg.EnableEmployeePhotoSource = overlay.EnableEmployeePhotoSource; if (overlay.EmployeePhotoBaseUrl != null) cfg.EmployeePhotoBaseUrl = overlay.EmployeePhotoBaseUrl; @@ -684,6 +693,9 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase [DataMember] public List InitialSyncEmployeeIds { get; set; } = new List(); + [DataMember(EmitDefaultValue = false)] + public int? InitialSyncLocationSiteId { get; set; } + [DataMember] public bool EnableEmployeePhotoSource { get; set; } @@ -773,6 +785,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase private readonly string _attendanceBizDirectory; private readonly string _templateBizDirectory; private readonly string _userSyncBizDirectory; + private readonly string _departmentalSyncBizDirectory; private readonly string _unreachableBizDirectory; private readonly object _sync = new(); public readonly BizSessionTotals Totals = new BizSessionTotals(); @@ -786,11 +799,13 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase _attendanceBizDirectory = Path.Combine(logsRoot, "attendance_logs"); _templateBizDirectory = Path.Combine(logsRoot, "template_fetching_logs"); _userSyncBizDirectory = Path.Combine(logsRoot, "user_sync_logs"); + _departmentalSyncBizDirectory = Path.Combine(logsRoot, "departmental_sync_logs"); _unreachableBizDirectory = Path.Combine(_internalLogsDirectory, "unreachable_devices"); Directory.CreateDirectory(_internalLogsDirectory); Directory.CreateDirectory(_attendanceBizDirectory); Directory.CreateDirectory(_templateBizDirectory); Directory.CreateDirectory(_userSyncBizDirectory); + Directory.CreateDirectory(_departmentalSyncBizDirectory); Directory.CreateDirectory(_unreachableBizDirectory); } @@ -858,6 +873,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase AppendBiz(BizChannel.Attendance, block); AppendBiz(BizChannel.UserSync, block); AppendBiz(BizChannel.Template, block); + AppendBiz(BizChannel.DepartmentalSync, block); AppendBiz(BizChannel.Unreachable, block); } @@ -873,6 +889,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase AppendBiz(BizChannel.Attendance, stopped); AppendBiz(BizChannel.UserSync, stopped); AppendBiz(BizChannel.Template, stopped); + AppendBiz(BizChannel.DepartmentalSync, stopped); AppendBiz(BizChannel.Unreachable, stopped); } @@ -905,6 +922,9 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase case BizChannel.Template: path = Path.Combine(_templateBizDirectory, "template_fetching_logs_" + DateStamp() + ".txt"); break; + case BizChannel.DepartmentalSync: + path = Path.Combine(_departmentalSyncBizDirectory, "departmental_sync_logs_" + DateStamp() + ".txt"); + break; case BizChannel.Unreachable: path = Path.Combine(_unreachableBizDirectory, "unreachable_devices_" + DateStamp() + ".txt"); break;