From ec6aaac0fa61df190f9e2af63735e01a83e2e10c Mon Sep 17 00:00:00 2001 From: "mustafa.ahmed" Date: Tue, 25 Aug 2026 10:30:59 +0500 Subject: [PATCH] feat: require location-site filter and add departmental sync summaries Initial department sync now requires InitialSyncLocationSiteId and can run across all active departments when department IDs are empty. Employee selection filters by location site, reports inactive/missing departments, and writes per-department outcomes to a dedicated DepartmentalSync biz log. --- BusinessLogging.cs | 1 + DeviceSettings.config | 9 +- HikvisionAttendanceManager.InitialSync.cs | 462 +++++++++++++++++++--- HikvisionAttendanceWindowsService.cs | 20 + 4 files changed, 436 insertions(+), 56 deletions(-) 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;