diff --git a/HrmsEmployeeDAO.cs b/HrmsEmployeeDAO.cs new file mode 100644 index 0000000..b55e2bd --- /dev/null +++ b/HrmsEmployeeDAO.cs @@ -0,0 +1,130 @@ +using MySql.Data.MySqlClient; +using System; + +namespace HanvonF710XAttendanceService +{ + /// + /// HRMS employee row used for Hanvon display name and portal photo URL. + /// Device enroll/serial must NOT be treated as employee.id. + /// + internal sealed class HrmsEmployeeInfo + { + public HrmsEmployeeInfo(string employeeId, string serialNumberInt, string concatenatedName, string department = "") + { + EmployeeId = employeeId ?? ""; + SerialNumberInt = serialNumberInt ?? ""; + ConcatenatedName = concatenatedName ?? ""; + Department = department ?? ""; + } + + /// employee.id — used for portal photo URL only. + public string EmployeeId { get; } + + /// employee.serial_number_int — matches device / attendance_machine_user.serial_number. + public string SerialNumberInt { get; } + + /// employee.concatenated_name — Hanvon user display name. + public string ConcatenatedName { get; } + + /// Department display name when available; otherwise department_id. + public string Department { get; } + } + + internal sealed class HrmsEmployeeDAO + { + /// + /// Match device serial (attendance_machine_user.serial_number) to employee.serial_number_int. + /// + public bool TryGetByDeviceSerial(MySqlConnection connection, string deviceSerial, out HrmsEmployeeInfo employee) + { + employee = null; + if (connection == null || string.IsNullOrWhiteSpace(deviceSerial)) + { + return false; + } + + string serial = deviceSerial.Trim(); + + // Prefer department name; fall back to department_id if departments table/join is unavailable. + const string queryWithDeptName = + "SELECT e.id, e.serial_number_int, e.concatenated_name, " + + "COALESCE(NULLIF(TRIM(d.name), ''), CAST(e.department_id AS CHAR), '') AS department " + + "FROM hrms.employee e " + + "LEFT JOIN hrms.departments d ON d.id = e.department_id " + + "WHERE CAST(e.serial_number_int AS CHAR) = @DeviceSerial " + + "LIMIT 1"; + + const string queryBasic = + "SELECT e.id, e.serial_number_int, e.concatenated_name, " + + "CAST(e.department_id AS CHAR) AS department " + + "FROM hrms.employee e " + + "WHERE CAST(e.serial_number_int AS CHAR) = @DeviceSerial " + + "LIMIT 1"; + + try + { + if (TryReadEmployee(connection, queryWithDeptName, serial, out employee)) + { + return true; + } + } + catch + { + // departments table/column may differ; fall back. + } + + return TryReadEmployee(connection, queryBasic, serial, out employee); + } + + private static bool TryReadEmployee(MySqlConnection connection, string query, string serial, out HrmsEmployeeInfo employee) + { + employee = null; + using (var cmd = new MySqlCommand(query, connection)) + { + cmd.CommandTimeout = 60; + cmd.Parameters.AddWithValue("@DeviceSerial", serial); + using (var reader = cmd.ExecuteReader()) + { + if (!reader.Read()) + { + return false; + } + + string id = reader.IsDBNull(0) ? "" : Convert.ToString(reader.GetValue(0))?.Trim(); + string serialInt = reader.IsDBNull(1) ? "" : Convert.ToString(reader.GetValue(1))?.Trim(); + string name = reader.IsDBNull(2) ? "" : Convert.ToString(reader.GetValue(2))?.Trim(); + string department = reader.FieldCount > 3 && !reader.IsDBNull(3) + ? Convert.ToString(reader.GetValue(3))?.Trim() + : ""; + + if (string.IsNullOrWhiteSpace(id)) + { + return false; + } + + employee = new HrmsEmployeeInfo(id, serialInt, name, department); + return true; + } + } + } + + public string ResolveDisplayName( + MySqlConnection connection, + string deviceSerial, + string fallbackName = null) + { + if (TryGetByDeviceSerial(connection, deviceSerial, out HrmsEmployeeInfo employee) + && !string.IsNullOrWhiteSpace(employee.ConcatenatedName)) + { + return employee.ConcatenatedName; + } + + if (!string.IsNullOrWhiteSpace(fallbackName)) + { + return fallbackName.Trim(); + } + + return string.IsNullOrWhiteSpace(deviceSerial) ? "" : deviceSerial.Trim(); + } + } +} diff --git a/HrmsEmployeeMappingTests.cs b/HrmsEmployeeMappingTests.cs new file mode 100644 index 0000000..0635cd0 --- /dev/null +++ b/HrmsEmployeeMappingTests.cs @@ -0,0 +1,66 @@ +using System; + +namespace HanvonF710XAttendanceService +{ + internal static class HrmsEmployeeMappingTests + { + public static int RunAll() + { + int failed = 0; + failed += Run("photo URL uses employees.id not device serial", TestPhotoUrlUsesEmployeeId); + failed += Run("display name prefers concatenated_name", TestDisplayNamePrefersConcatenatedName); + failed += Run("display name falls back to device serial", TestDisplayNameFallback); + return failed; + } + + private static int Run(string name, Func test) + { + try + { + if (!test()) + { + Console.WriteLine("FAIL: " + name); + return 1; + } + + Console.WriteLine("PASS: " + name); + return 0; + } + catch (Exception ex) + { + Console.WriteLine("FAIL: " + name + " err=" + ex.Message); + return 1; + } + } + + private static bool TestPhotoUrlUsesEmployeeId() + { + // Device serial=15057 maps to employees.id=84321 for photo URL only. + string url = HrmsEmployeePhotoClient.BuildPhotoUrl( + "https://portal.utopiaindustries.pk/uind/employee-photo/", + "84321"); + return string.Equals( + url, + "https://portal.utopiaindustries.pk/uind/employee-photo/84321.jpeg", + StringComparison.OrdinalIgnoreCase); + } + + private static bool TestDisplayNamePrefersConcatenatedName() + { + var info = new HrmsEmployeeInfo("84321", "15057", "John Smith"); + return info.EmployeeId == "84321" + && info.SerialNumberInt == "15057" + && info.ConcatenatedName == "John Smith" + && info.EmployeeId != info.SerialNumberInt; + } + + private static bool TestDisplayNameFallback() + { + var info = new HrmsEmployeeInfo("84321", "15057", ""); + string name = !string.IsNullOrWhiteSpace(info.ConcatenatedName) + ? info.ConcatenatedName + : "15057"; + return name == "15057"; + } + } +}