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";
+ }
+ }
+}