206 lines
11 KiB
C#
206 lines
11 KiB
C#
using HikvisionAttendanceManager.App.Models;
|
|
using MySql.Data.MySqlClient;
|
|
|
|
namespace HikvisionAttendanceManager.App.Services;
|
|
|
|
/// <summary>HRMS reader aligned with UIND schema (employee.concatenated_name, is_active, department.title).</summary>
|
|
public sealed class HrmsEmployeeService
|
|
{
|
|
/// <summary>Display name expression used across all employee queries.</summary>
|
|
private const string EmployeeNameExpression =
|
|
"COALESCE(NULLIF(TRIM(e.concatenated_name), ''), TRIM(CONCAT(COALESCE(e.first_name,''), ' ', COALESCE(e.middle_name,''), ' ', COALESCE(e.last_name,''))))";
|
|
|
|
private const string EmployeeQuery = $"""
|
|
SELECT e.id,e.serial_number,{EmployeeNameExpression} AS employee_display_name,
|
|
e.department_id,e.is_active,e.location_site_id,e.has_photo,
|
|
d.title AS department_name,d.is_active AS department_is_active
|
|
FROM hrms.employee e
|
|
LEFT JOIN hrms.department d ON d.id = e.department_id
|
|
""";
|
|
|
|
public async Task<IReadOnlyList<LocationSite>> GetLocationSitesAsync(CancellationToken cancellationToken)
|
|
{
|
|
const string sql = """
|
|
SELECT DISTINCT e.location_site_id,
|
|
COALESCE(ls.title, CONCAT('Site ', e.location_site_id)) AS site_title
|
|
FROM hrms.employee e
|
|
LEFT JOIN inventory.location_site ls ON ls.id = e.location_site_id
|
|
WHERE e.location_site_id IS NOT NULL
|
|
ORDER BY site_title
|
|
""";
|
|
return await QuerySitesAsync(sql, null, cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<HrmsDepartment>> GetActiveDepartmentsBySiteAsync(long siteId, CancellationToken cancellationToken)
|
|
{
|
|
const string inventorySql = """
|
|
SELECT DISTINCT d.id, d.title, d.is_active
|
|
FROM hrms.department d
|
|
INNER JOIN inventory.department_location_site dls ON dls.department_id = d.id
|
|
WHERE d.is_active = 1 AND dls.site_id = @siteId
|
|
ORDER BY d.title
|
|
""";
|
|
try
|
|
{
|
|
var departments = await QueryDepartmentsAsync(inventorySql, cmd => cmd.Parameters.AddWithValue("@siteId", siteId), cancellationToken);
|
|
if (departments.Count > 0) return departments;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Warning("Department load via inventory.department_location_site failed; falling back to employee-derived departments. " + ex.Message);
|
|
}
|
|
|
|
const string fallbackSql = """
|
|
SELECT DISTINCT d.id, d.title, d.is_active
|
|
FROM hrms.department d
|
|
INNER JOIN hrms.employee e ON e.department_id = d.id
|
|
WHERE d.is_active = 1 AND e.is_active = 1 AND e.location_site_id = @siteId
|
|
ORDER BY d.title
|
|
""";
|
|
return await QueryDepartmentsAsync(fallbackSql, cmd => cmd.Parameters.AddWithValue("@siteId", siteId), cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<HrmsDepartment>> GetActiveDepartmentsAsync(CancellationToken cancellationToken) =>
|
|
await QueryDepartmentsAsync("SELECT id, title, is_active FROM hrms.department WHERE is_active = 1 ORDER BY title", null, cancellationToken);
|
|
|
|
public async Task<IReadOnlyList<HrmsEmployee>> GetEmployeesForDepartmentsAsync(IEnumerable<long> departmentIds, long siteId, CancellationToken cancellationToken)
|
|
{
|
|
var ids = departmentIds.Distinct().ToList();
|
|
if (ids.Count == 0) return [];
|
|
var placeholders = string.Join(",", ids.Select((_, i) => "@d" + i));
|
|
var sql = EmployeeQuery + $" WHERE e.department_id IN ({placeholders}) AND e.is_active = 1 AND d.is_active = 1 AND e.location_site_id = @siteId ORDER BY employee_display_name";
|
|
return await QueryEmployeesAsync(sql, command =>
|
|
{
|
|
for (var i = 0; i < ids.Count; i++)
|
|
command.Parameters.AddWithValue("@d" + i, ids[i]);
|
|
command.Parameters.AddWithValue("@siteId", siteId);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<HrmsEmployee>> GetActiveEmployeesBySiteAsync(long siteId, CancellationToken cancellationToken) =>
|
|
await QueryEmployeesAsync(EmployeeQuery + " WHERE e.is_active = 1 AND e.location_site_id = @siteId ORDER BY employee_display_name",
|
|
command => command.Parameters.AddWithValue("@siteId", siteId), cancellationToken);
|
|
|
|
public async Task<IReadOnlyList<HrmsEmployee>> SearchEmployeesAsync(string query, long? siteId, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(query)) return [];
|
|
var sql = EmployeeQuery + $" WHERE e.is_active = 1 AND (e.serial_number LIKE @q OR {EmployeeNameExpression} LIKE @q OR e.concatenated_name LIKE @q)";
|
|
if (siteId.HasValue) sql += " AND e.location_site_id = @siteId";
|
|
sql += " ORDER BY employee_display_name LIMIT 25";
|
|
return await QueryEmployeesAsync(sql, command =>
|
|
{
|
|
command.Parameters.AddWithValue("@q", "%" + query.Trim() + "%");
|
|
if (siteId.HasValue) command.Parameters.AddWithValue("@siteId", siteId.Value);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
public async Task<HrmsEmployee?> FindBySerialNumberAsync(string serialNumber, CancellationToken cancellationToken)
|
|
{
|
|
var sql = EmployeeQuery + " WHERE e.serial_number = @serialNumber LIMIT 1";
|
|
return await QueryOneAsync(sql, command => command.Parameters.AddWithValue("@serialNumber", serialNumber), cancellationToken);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<HrmsEmployee>> GetEmployeesForDepartmentAsync(long departmentId, long siteId, CancellationToken cancellationToken) =>
|
|
await QueryEmployeesAsync(EmployeeQuery + " WHERE e.department_id = @departmentId AND e.is_active = 1 AND d.is_active = 1 AND e.location_site_id = @siteId ORDER BY employee_display_name",
|
|
command => { command.Parameters.AddWithValue("@departmentId", departmentId); command.Parameters.AddWithValue("@siteId", siteId); }, cancellationToken);
|
|
|
|
public async Task<IReadOnlyList<HrmsEmployee>> GetEmployeesForAllActiveDepartmentsAsync(long siteId, CancellationToken cancellationToken) =>
|
|
await QueryEmployeesAsync(EmployeeQuery + " WHERE e.is_active = 1 AND d.is_active = 1 AND e.location_site_id = @siteId ORDER BY employee_display_name",
|
|
command => command.Parameters.AddWithValue("@siteId", siteId), cancellationToken);
|
|
|
|
private async Task<HrmsEmployee?> QueryOneAsync(string sql, Action<MySqlCommand>? parameters, CancellationToken cancellationToken)
|
|
{
|
|
var list = await QueryEmployeesAsync(sql, parameters, cancellationToken);
|
|
return list.FirstOrDefault();
|
|
}
|
|
|
|
private async Task<IReadOnlyList<HrmsEmployee>> QueryEmployeesAsync(string sql, Action<MySqlCommand>? parameters, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var employees = new List<HrmsEmployee>();
|
|
await using var connection = HrmsConnectionFactory.CreateConnection();
|
|
await connection.OpenAsync(cancellationToken);
|
|
await using var command = new MySqlCommand(sql, connection);
|
|
parameters?.Invoke(command);
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
employees.Add(MapEmployee(reader));
|
|
return employees;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Error("HRMS employee query failed.", ex);
|
|
throw new HrmsDataException("Unable to load employees. Please check the HRMS connection.", ex);
|
|
}
|
|
}
|
|
|
|
private static async Task<IReadOnlyList<HrmsDepartment>> QueryDepartmentsAsync(string sql, Action<MySqlCommand>? parameters, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var departments = new List<HrmsDepartment>();
|
|
await using var connection = HrmsConnectionFactory.CreateConnection();
|
|
await connection.OpenAsync(cancellationToken);
|
|
await using var command = new MySqlCommand(sql, connection);
|
|
parameters?.Invoke(command);
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
departments.Add(new HrmsDepartment(
|
|
Convert.ToInt64(reader["id"]),
|
|
reader["title"]?.ToString() ?? "",
|
|
reader["is_active"] != DBNull.Value && Convert.ToInt32(reader["is_active"]) == 1));
|
|
}
|
|
return departments;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Error("HRMS department query failed.", ex);
|
|
throw new HrmsDataException("Unable to load departments. Please retry.", ex);
|
|
}
|
|
}
|
|
|
|
private static async Task<IReadOnlyList<LocationSite>> QuerySitesAsync(string sql, Action<MySqlCommand>? parameters, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var sites = new List<LocationSite>();
|
|
await using var connection = HrmsConnectionFactory.CreateConnection();
|
|
await connection.OpenAsync(cancellationToken);
|
|
await using var command = new MySqlCommand(sql, connection);
|
|
parameters?.Invoke(command);
|
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
var id = Convert.ToInt64(reader["location_site_id"]);
|
|
var title = reader["site_title"]?.ToString();
|
|
sites.Add(new LocationSite(id, string.IsNullOrWhiteSpace(title) ? $"Site {id}" : title.Trim()));
|
|
}
|
|
return sites;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLogger.Error("HRMS location site query failed.", ex);
|
|
throw new HrmsDataException("Unable to load location sites. Please check the HRMS connection.", ex);
|
|
}
|
|
}
|
|
|
|
private static HrmsEmployee MapEmployee(System.Data.Common.DbDataReader reader) =>
|
|
new(
|
|
Convert.ToInt64(reader["id"]),
|
|
reader["serial_number"]?.ToString() ?? "",
|
|
reader["employee_display_name"]?.ToString()?.Trim() ?? "",
|
|
reader["department_id"] == DBNull.Value ? null : Convert.ToInt64(reader["department_id"]),
|
|
reader["is_active"] != DBNull.Value && Convert.ToInt32(reader["is_active"]) == 1,
|
|
reader["location_site_id"] == DBNull.Value ? null : Convert.ToInt64(reader["location_site_id"]),
|
|
reader["department_name"]?.ToString() ?? "",
|
|
reader["department_is_active"] != DBNull.Value && Convert.ToInt32(reader["department_is_active"]) == 1,
|
|
reader["has_photo"] != DBNull.Value && Convert.ToInt32(reader["has_photo"]) == 1);
|
|
}
|
|
|
|
public sealed class HrmsDataException : Exception
|
|
{
|
|
public HrmsDataException(string userMessage, Exception inner) : base(userMessage, inner) { }
|
|
}
|