uind-hikvision-attendance-m.../src/HikvisionAttendanceManager.App/Services/HrmsEmployeeService.cs

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) { }
}