using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GB5Exception; using GB5Shared.GenerateAutoNumber; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PayRollBLL.Adjustment; using PayRollBLL.PayProcess; using PayRollDAL.CustomeCode.Leave; using PayRollDAL.DTO.Adjustment; using PayRollDAL.DTO.Leave; using PayRollDAL.DTO.PayProcess; using PayRollDAL.DTO.TLeave; using PayRollDAL.Query.Employee; using System.Text.Json; using static GB5Shared.GB5Constant.Constant; namespace PayRollBLL.Leave { public class LeaveBLL : ILeaveBLL { private readonly ILeaveDAL _LeaveDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly EventLogPublish _eventLogPublish; // Your existing private readonly IPayProcessBLL _PayProcessBLL; private readonly IAdjustmentBLL _AdjustmentBLL; private readonly ILogger _logger; public LeaveBLL( ILeaveDAL leaveDAL, AutoNumber autoNumber, IValidation validation, EventLogPublish eventLogPublish, IPayProcessBLL payProcessBLL, IAdjustmentBLL adjustmentBLL, ILogger ILogger) { _LeaveDAL = leaveDAL; _AutoNumber = autoNumber; _Validation = validation; _eventLogPublish = eventLogPublish; _PayProcessBLL = payProcessBLL; _AdjustmentBLL = adjustmentBLL; _logger = ILogger; } public async Task GetLeave(int LeaveId, LoginDTO LoginDTO) { try { return await _LeaveDAL.GetLeave(LeaveId, LoginDTO); } catch (Exception) { throw; } } public async Task SaveLeave(LeaveDTO leaveDTO, LoginDTO loginDTO) { if (leaveDTO == null) throw new ArgumentNullException(nameof(leaveDTO)); if (leaveDTO.LeaveTenantId == 0) leaveDTO.LeaveTenantId = loginDTO.ClientId; if (leaveDTO.LeaveTenantId == null) throw new Exception("TenantId cannot be null."); await _Validation.NotEmpty(leaveDTO.LeaveCode, nameof(leaveDTO.LeaveCode)); await _Validation.NotEmpty(leaveDTO.LeaveName, nameof(leaveDTO.LeaveName)); leaveDTO.LeaveModifiedById = loginDTO.UserId; leaveDTO.LeaveModifiedOn = DateTime.UtcNow; leaveDTO.LeaveStatus = 1; // Active — MLEAVE has no approval workflow to promote it later bool isCreate = leaveDTO.LeaveId == 0; int leaveId; if (isCreate) { var auto = await _AutoNumber.GetAutoNumber(1, "LEAVE", loginDTO); leaveDTO.LeaveId = auto.StartNumber; leaveDTO.LeaveCreatedById = loginDTO.UserId; leaveDTO.LeaveCreatedOn = DateTime.UtcNow; leaveId = await _LeaveDAL.SaveLeave(leaveDTO, loginDTO); } else { leaveId = await _LeaveDAL.UpdateLeave(leaveDTO, loginDTO); } // ================= EVENT PUBLISH (AFTER DB COMMIT) ================= string action = isCreate ? "Created" : "Updated"; await _eventLogPublish.PublishEventLogAsync( EventText: $"Leave {action} with ID: {leaveDTO.LeaveId}, Code: {leaveDTO.LeaveCode}", entity: leaveDTO, // FULL DTO payload EventTypeId: GB5Shared.GB5Constant.Constant.EventTypeConstant.SAVELEAVEEVENTTYPEID, DataId: leaveDTO.LeaveId, loginDTO: loginDTO, topic: "entity-saved-topic" // Generic orchestration topic ); return isCreate ? $"Leave saved successfully." : $"Leave updated successfully."; } public async Task DeleteLeave(int LeaveId, LoginDTO LoginDTO) { try { return await _LeaveDAL.DeleteLeave(LeaveId, LoginDTO); } catch (Exception) { throw; } } public async Task GetSelectListLeave(CriteriaDTO criteriaDTO, LoginDTO LoginDTO) { try { return await _LeaveDAL.GetSelectListLeave(criteriaDTO, LoginDTO); } catch (Exception) { throw; } } public async Task GetRootsEmployeePermissionInfo(int EmployeeId, DateTime Date, LoginDTO LoginDTO) { try { return await _LeaveDAL.GetRootsEmployeePermissionInfo(EmployeeId, Date, LoginDTO); } catch (Exception) { throw; } } public async Task GetSelectListMLeave(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _LeaveDAL.GetSelectListMLeave(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); } catch (Exception) { throw; } } public async Task> GetLeaveBalanceAsync(DateTime periodFrom, DateTime periodTo, LoginDTO loginDTO) { var criteria = new CriteriaDTO { SectionCriteriaList = new List { new SectionCriteriaDTO { AttributesCriteriaList = new List { new AttributesCriteriaDTO { FieldName = "PeriodFrom", FieldValue = periodFrom.ToString("yyyy-MM-dd HH:mm:ss") }, new AttributesCriteriaDTO { FieldName = "PeriodTo", FieldValue = periodTo.ToString("yyyy-MM-dd HH:mm:ss") }, new AttributesCriteriaDTO { FieldName = "EmployeeId", FieldValue = loginDTO.UserId.ToString() }, new AttributesCriteriaDTO { FieldName = "OUId", FieldValue = loginDTO.WorkOUId.ToString() } } } } }; string json = await _PayProcessBLL.LeaveStatusReportAsync(1, int.MaxValue, criteria, loginDTO); return string.IsNullOrEmpty(json) ? new List() : Newtonsoft.Json.JsonConvert.DeserializeObject>(json) ?? new List(); } public async Task> GetLeaveBalanceByUserCodeAsync(DateTime periodFrom, DateTime periodTo, LoginDTO loginDTO) { if (string.IsNullOrWhiteSpace(loginDTO.UserCode)) throw new ArgumentException("UserCode is missing in the login session."); int employeeId = await _LeaveDAL.GetEmployeeIdByUserCodeAsync(loginDTO.UserCode, loginDTO); if (employeeId == 0) throw new ArgumentException($"No employee found for UserCode '{loginDTO.UserCode}'."); var criteria = new CriteriaDTO { SectionCriteriaList = new List { new SectionCriteriaDTO { AttributesCriteriaList = new List { new AttributesCriteriaDTO { FieldName = "PeriodFrom", FieldValue = periodFrom.ToString("yyyy-MM-dd HH:mm:ss") }, new AttributesCriteriaDTO { FieldName = "PeriodTo", FieldValue = periodTo.ToString("yyyy-MM-dd HH:mm:ss") }, new AttributesCriteriaDTO { FieldName = "EmployeeId", FieldValue = employeeId.ToString() }, new AttributesCriteriaDTO { FieldName = "OUId", FieldValue = loginDTO.WorkOUId.ToString() } } } } }; string json = await _PayProcessBLL.LeaveStatusReportAsync(1, int.MaxValue, criteria, loginDTO); return string.IsNullOrEmpty(json) ? new List() : Newtonsoft.Json.JsonConvert.DeserializeObject>(json) ?? new List(); } /// /// Master Orchestrator Method /// #region 🔹 Public Method #region 🔹 Public Method public async Task> GetAllLeaveDetailsInOneAsync( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO) { if (criteriaDTO == null || criteriaDTO.SectionCriteriaList?.Count == 0) throw new ArgumentException("CriteriaDTO cannot be null or empty"); _logger.LogInformation("Starting GetAllLeaveDetailsInOneAsync for user {UserId}", loginDTO?.UserId); try { DateTime? periodFrom = null; DateTime? periodTo = null; int? employeeId = null; int? ouId = null; int? departmentId = null; // ✅ Extract Criteria (GB4 STYLE) foreach (var section in criteriaDTO.SectionCriteriaList ?? Enumerable.Empty()) { foreach (var attr in section.AttributesCriteriaList ?? Enumerable.Empty()) { if (attr?.FieldName == null || attr.FieldValue == null) continue; var field = attr.FieldName.Trim().ToLower(); switch (field) { case "periodfrom": periodFrom = GetDate(attr.FieldValue); if (periodFrom.HasValue) attr.FieldValue = periodFrom.Value.ToString("yyyy-MM-dd HH:mm:ss"); break; case "periodto": periodTo = GetDate(attr.FieldValue); if (periodTo.HasValue) attr.FieldValue = periodTo.Value.ToString("yyyy-MM-dd HH:mm:ss"); break; case "employeeid": employeeId = GetLong(attr.FieldValue) is long emp && emp != 0 ? (int)emp : null; break; case "ouid": ouId = GetLong(attr.FieldValue) is long ou && ou != 0 ? (int)ou : null; break; case "departmentid": departmentId = GetLong(attr.FieldValue) is long dept && dept != 0 ? (int)dept : null; break; } } } _logger.LogInformation( "Filters → From: {From}, To: {To}, Emp: {Emp}, OU: {OU}, Dept: {Dept}", periodFrom, periodTo, employeeId, ouId, departmentId); // ✅ Parallel Calls (NO ContinueWith) var employeeTask = SafeFetch( _LeaveDAL.GetEmployeeDetailsAsync(loginDTO, employeeId, ouId, departmentId), "Employees"); var permissionTask = SafeFetch( _LeaveDAL.GetEmployeePermissionDetailsAsync(loginDTO, periodFrom, periodTo, employeeId, ouId, departmentId), "Permissions"); var creditDebitTask = SafeFetch( _LeaveDAL.GetEmployeeLeaveCreditDebitDetailsAsync(loginDTO, periodFrom, periodTo, employeeId, ouId, departmentId), "CreditDebits"); var requestTask = SafeFetch( _LeaveDAL.GetEmployeeLeaveRequestDetailsAsync(loginDTO, periodFrom, periodTo, employeeId, ouId, departmentId), "Requests"); var adjustmentTask = SafeFetch( ConvertToEnumerable(_AdjustmentBLL.GetAdjustmentReportAsync(-1, -1, criteriaDTO, loginDTO, false)), "Adjustments"); var attendanceTask = SafeFetch( _LeaveDAL.GetEmployeeDailyAttendanceAsync(loginDTO, periodFrom, periodTo, employeeId, ouId, departmentId), "Attendances"); var leaveStatusTask = SafeFetchString( _PayProcessBLL.LeaveStatusReportAsync(firstNumber, maxResult, criteriaDTO, loginDTO), "LeaveStatus"); var leaveOnDutyTask = SafeFetch( _LeaveDAL.GetEmployeeLeaveOnDutyRequestAsync(loginDTO, periodFrom, periodTo, employeeId, ouId, departmentId), "LeaveOnDuties"); var missedPunchTask = SafeFetch( _LeaveDAL.GetEmployeeMissedPunchAsync( loginDTO, periodFrom, periodTo, employeeId, "PUNCH" ), "MissedPunch"); var punchDetailTask = SafeFetch( _LeaveDAL.GetEmployeePunchDetailsAsync(loginDTO, periodFrom, periodTo, employeeId, ouId, departmentId), "PunchDetails"); await Task.WhenAll( employeeTask, permissionTask, creditDebitTask, requestTask, leaveStatusTask, adjustmentTask, attendanceTask, leaveOnDutyTask, missedPunchTask, punchDetailTask ); // ✅ Collect var employees = await employeeTask; var permissions = ApplyPermissionFilter(await permissionTask, periodFrom, periodTo); var creditDebits = ApplyLeaveFilter(await creditDebitTask, periodFrom, periodTo); var requests = ApplyLeaveRequestFilter(await requestTask, periodFrom, periodTo); var leaveStatusJson = await leaveStatusTask; var adjustmentList = await adjustmentTask; var attendances = await attendanceTask; var leaveOnDuties = await leaveOnDutyTask; var missedPunches = ApplyMissedPunchFilter(await missedPunchTask, employees, periodFrom, periodTo); var punchDetails = await punchDetailTask; // ✅ Pagination employees = ApplyPagination(employees, firstNumber, maxResult); // ✅ Deserialize var leaveStatusList = SafeDeserialize(leaveStatusJson); // ✅ Lookup var permissionLookup = BuildLookup(permissions, x => x.EmployeeId); var creditLookup = BuildLookup(creditDebits, x => x.EmployeeId); var requestLookup = BuildLookup(requests, x => x.EmployeeId); var statusLookup = BuildLookup(leaveStatusList, x => x.EmployeeId); var adjustmentLookup = BuildLookup(adjustmentList, x => x.EmployeeId); var attendanceLookup = BuildLookup(attendances, x => x.EmployeeId); var dutyLookup = BuildLookup(leaveOnDuties, x => x.EmployeeId); var missedLookup = BuildLookup(missedPunches, x => x.EmployeeId); var punchLookup = BuildLookup(punchDetails, x => x.EmployeeId); // ✅ Mapping foreach (var emp in employees) { emp.PermssionDetailDTOs = GetOrEmpty(permissionLookup, emp.EmployeeId); emp.LeaveCreditDebitDetailDTOs = GetOrEmpty(creditLookup, emp.EmployeeId); emp.LeaveRequestDebitDetailDTOs = GetOrEmpty(requestLookup, emp.EmployeeId); emp.LeaveStatusDTOs = GetOrEmpty(statusLookup, emp.EmployeeId); emp.AdjustmentReportDTOs = GetOrEmpty(adjustmentLookup, emp.EmployeeId); emp.DailyAttendanceDTOs = GetOrEmpty(attendanceLookup, emp.EmployeeId); emp.LeaveOnDutyRequestDTOs = GetOrEmpty(dutyLookup, emp.EmployeeId); emp.MissedPunchDTOs = GetOrEmpty(missedLookup, emp.EmployeeId); emp.PunchDetailDTOs = GetOrEmpty(punchLookup, emp.EmployeeId); } return employees; } catch (Exception ex) { _logger.LogError(ex, "Error fetching leave details"); throw; } } #endregion #endregion #region 🔹 Private Helper Methods private void NormalizeDateCriteria(CriteriaDTO criteriaDTO) { foreach (var section in criteriaDTO.SectionCriteriaList) { foreach (var attr in section.AttributesCriteriaList ?? Enumerable.Empty()) { if (attr?.FieldName == null || attr.FieldValue == null) continue; var field = attr.FieldName.ToLower(); if (field == "periodfrom" || field == "periodto") { var dt = GetDate(attr.FieldValue); if (dt.HasValue) attr.FieldValue = dt.Value.ToString("yyyy-MM-dd HH:mm:ss"); } } } } private DateTime? GetCriteriaDate(CriteriaDTO criteriaDTO, string field) { return criteriaDTO.SectionCriteriaList? .SelectMany(s => s.AttributesCriteriaList ?? new List()) .Where(a => a.FieldName?.Equals(field, StringComparison.OrdinalIgnoreCase) == true) .Select(a => GetDate(a.FieldValue)) .FirstOrDefault(); } private List ApplyPagination(List list, int first, int max) { if (first > 0 && max > 0) return list.Skip(first - 1).Take(max).ToList(); return list; } private List ApplyMissedPunchFilter( List data, List employees, DateTime? from, DateTime? to) { if (data == null || data.Count == 0) return new List(); var validIds = employees.Select(e => e.EmployeeId).ToHashSet(); return data.Where(x => { if (!validIds.Contains(x.EmployeeId)) return false; if (from.HasValue || to.HasValue) { if (!DateTime.TryParse(x.PunchAttendanceDate, out var dt)) return false; if (from.HasValue && dt < from) return false; if (to.HasValue && dt > to) return false; } return true; }).ToList(); } private List GetOrEmpty(Dictionary> dict, int key) { return dict.TryGetValue(key, out var val) ? val : new List(); } private async Task> SafeFetch(Task> task, string name) { try { var result = await task; return result?.ToList() ?? new List(); } catch (Exception ex) { _logger.LogError(ex, $"Error fetching {name}"); return new List(); } } private async Task SafeFetchString(Task task, string name) { try { return await task ?? "[]"; } catch (Exception ex) { _logger.LogError(ex, $"Error fetching {name}"); return "[]"; } } private DateTime? GetDate(object value) { if (value == null) return null; if (DateTime.TryParse(value.ToString(), out var dt)) return dt; return null; } private long GetLong(object value) { if (value == null) return 0; if (long.TryParse(value.ToString(), out var result)) return result; return 0; } private int? GetCriteriaInt(CriteriaDTO criteriaDTO, string field) { return criteriaDTO.SectionCriteriaList? .SelectMany(s => s.AttributesCriteriaList ?? new List()) .Where(a => a.FieldName?.Equals(field, StringComparison.OrdinalIgnoreCase) == true) .Select(a => { if (a.FieldValue == null) return (int?)null; if (int.TryParse(a.FieldValue.ToString(), out var val)) return val; return (int?)null; }) .FirstOrDefault(); } private List ApplyPermissionFilter( List list, DateTime? from, DateTime? to) { var fromDate = from.HasValue ? DateOnly.FromDateTime(from.Value) : (DateOnly?)null; var toDate = to.HasValue ? DateOnly.FromDateTime(to.Value) : (DateOnly?)null; return list.Where(x => { var dt = x.AttendanceDate; if (fromDate.HasValue && dt < fromDate) return false; if (toDate.HasValue && dt > toDate) return false; return true; }).ToList(); } private List ApplyLeaveFilter( List list, DateTime? from, DateTime? to) { var fromDate = from.HasValue ? DateOnly.FromDateTime(from.Value) : (DateOnly?)null; var toDate = to.HasValue ? DateOnly.FromDateTime(to.Value) : (DateOnly?)null; return list.Where(x => { var dt = x.FromDate; if (fromDate.HasValue && dt < fromDate) return false; if (toDate.HasValue && dt > toDate) return false; return true; }).ToList(); } private List ApplyLeaveRequestFilter( List list, DateTime? from, DateTime? to) { var fromDate = from.HasValue ? DateOnly.FromDateTime(from.Value) : (DateOnly?)null; var toDate = to.HasValue ? DateOnly.FromDateTime(to.Value) : (DateOnly?)null; return list.Where(x => { var dt = x.FromDate; if (fromDate.HasValue && dt < fromDate) return false; if (toDate.HasValue && dt > toDate) return false; return true; }).ToList(); } private Dictionary> BuildLookup(IEnumerable list, Func keySelector) { return list? .GroupBy(keySelector) .ToDictionary(g => g.Key, g => g.ToList()) ?? new Dictionary>(); } private async Task> ConvertToEnumerable(Task> task) { return await task; } private async Task> ConvertToEnumerable(Task> task) { return await task; } private List SafeDeserialize(string json) { try { return string.IsNullOrEmpty(json) ? new List() : Newtonsoft.Json.JsonConvert.DeserializeObject>(json); } catch { return new List(); } } #endregion } }