using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Validation; using Newtonsoft.Json; using PayRollDAL.CustomeCode.TimeSlip; using PayRollDAL.DTO.TimeSlip; using PayRollDAL.Query.TimeSlip; using System; using System.Collections.Generic; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace PayRollBLL.TimeSlip { public class TimeSlipBLL : ITimeSlipBLL { private readonly ITimeSlipDAL _TimeSlipDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly IQueryExecutor _QueryExecutor; private readonly BaseEntityAppService _baseEntityAppService; private readonly VoucherNumberService _voucherNumberService; public TimeSlipBLL(ITimeSlipDAL TimeSlipDAL, AutoNumber AutoNumber, IValidation validation, IQueryExecutor IQueryExecutor, BaseEntityAppService baseEntityAppService, VoucherNumberService voucherNumberService) { _TimeSlipDAL = TimeSlipDAL; _AutoNumber = AutoNumber; _Validation = validation; _QueryExecutor = IQueryExecutor; _baseEntityAppService = baseEntityAppService; _voucherNumberService = voucherNumberService; } public async Task GetTimeSlip(int TimeSlipId, LoginDTO LoginDTO) { try { return await _TimeSlipDAL.GetTimeSlip(TimeSlipId, LoginDTO); } catch (Exception) { throw; } } public async Task SaveTimeSlip(TimeSlipDTO dto, LoginDTO login) { AutoNumberDTO? autoNumber = null; try { ValidateTimeSlip(dto); bool isNew = dto.TimeSlipId == 0; dto.TimeSlipDate = DateOnly.FromDateTime(ConvertToDate(dto.TimeSlipDate)); dto.TimeSlipAttendanceDate = DateOnly.FromDateTime(ConvertToDate(dto.TimeSlipAttendanceDate)); // Calculate duration if not supplied (matches GB4 logic) if (dto.TimeSlipTimeSlipStartTime > 0 && dto.TimeSlipTimeSlipEndTime > 0 && dto.TimeSlipDuration == 0) dto.TimeSlipDuration = (short)(dto.TimeSlipTimeSlipEndTime - dto.TimeSlipTimeSlipStartTime); // Mail-template time strings — set before enrichment so event log text is consistent dto.TimeSlipToDurtionMailTemplate = MinutesIntoTime(dto.TimeSlipDuration); dto.TimeSlipFromTimeMailTemplate = MinutesIntoTime(dto.TimeSlipTimeSlipStartTime); dto.TimeSlipToTimeMailTemplate = MinutesIntoTime(dto.TimeSlipTimeSlipEndTime); dto.TimeSlipDurationString = MinutesIntoTime(dto.TimeSlipDuration) + " Hrs"; dto.TimeSlipTimeSlipStartTimeString = MinutesIntoTime(dto.TimeSlipTimeSlipStartTime) + " Hrs"; dto.TimeSlipTimeSlipEndTimeString = MinutesIntoTime(dto.TimeSlipTimeSlipEndTime) + " Hrs"; dto.TimeSlipTimeSlipPermissionTypeString = dto.TimeSlipType == 0 ? "OFFICIAL" : "PERSONAL"; dto.TimeSlipAttendanceDateformailtemplate = dto.TimeSlipAttendanceDate.ToString("dd-MMM-yyyy"); dto.TimeSlipdateForMailTemplate = dto.TimeSlipDate.ToString("dd-MMM-yyyy"); dto.TimeSlipModifiedById = login.UserId; dto.TimeSlipModifiedOn = DateTime.UtcNow; autoNumber = isNew ? await _AutoNumber.GetNumberAsync(1, "TIMESLIP", login) : null; var trans = await _QueryExecutor.BeginTransactionAsync(login); try { /* ═══ RULE 1: DUPLICATE TIME OVERLAP ═══ */ var duplicate = await _TimeSlipDAL.CheckDuplicateAsync(dto, login, trans); if (duplicate != null && (isNew || duplicate.TimeSlipId != dto.TimeSlipId)) throw new Exception( $"Cannot save timeslip, already entry found with Number: {duplicate.TimeSlipNumber} " + $"from {duplicate.TempStartTime} to {duplicate.TempEndTime}"); /* ═══ RULE 2: DATE IN FINANCIAL PERIOD ═══ */ var period = await _TimeSlipDAL.GetPeriodAsync(dto.PeriodId, login, trans); string json = JsonConvert.SerializeObject(period); if (period == null) throw new Exception($"Period (Id={dto.PeriodId}) not found."); if (dto.TimeSlipDate < DateOnly.FromDateTime(period.PeriodFromDate) || dto.TimeSlipDate > DateOnly.FromDateTime(period.PeriodToDate)) throw new Exception("TimeSlip Date is Not in Financial Range"); /* ═══ RULE 3: PERMISSION LIMITS ═══ */ //var permissionMsg = await _TimeSlipDAL.CheckPermissionAsync(dto, login, trans); //if (!string.IsNullOrEmpty(permissionMsg)) // throw new Exception(permissionMsg); /* ═══ EMPLOYEE ENRICHMENT (GB4: GenericDal.Get) ═══ */ var emp = await _TimeSlipDAL.GetEmployeeDataAsync(dto.EmployeeId, login, trans); if (emp != null) { dto.EmployeeCode = emp.EmployeeCode; dto.EmployeeName = emp.EmployeeName; dto.EmployeeMobileNumber = emp.EmployeeMobileNumber; // Default shift from employee when caller passes -1 (matches GB4 logic) if (dto.ShiftId <= 0) dto.ShiftId = emp.ShiftId; // Reporting-chain mail/mobile IDs for event-trigger notifications dto.FunctionReportingToMailId = emp.FunctionReportingToMailId; dto.FunctionReportingToMobileNumber = emp.FunctionReportingToMobileNumber; dto.FunctionReportingToOfFunctionReportingToMailId = emp.FunctionReportingToOfFunctionReportingToMailId; dto.FunctionReportingToOfFunctionReportingToMobileNumber = emp.FunctionReportingToOfFunctionReportingToMobileNumber; dto.AdminReportingToMailId = emp.AdminReportingToMailId; dto.AdminReportingToMobileNumber = emp.AdminReportingToMobileNumber; dto.FunctionAndAdminReportingToMailId = $"{emp.FunctionReportingToMailId};{emp.AdminReportingToMailId}"; dto.FunctionAndAdminReportingToMobileNumber = $"{emp.FunctionReportingToMobileNumber};{emp.AdminReportingToMobileNumber}"; dto.MailId = $"{login.UserPrimaryMailId};{dto.MailId};{emp.FunctionReportingToMailId}"; } /* ═══ SAVE via ExecuteSaveAsync ═══ */ await _baseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTTIMESLIP, EventTypeConstant.SAVETIMESLIPEVENTTYPEID, dto, login, async tx => { if (isNew) { dto.TimeSlipId = autoNumber!.StartNumber; dto.TimeSlipCreatedById = login.UserId; dto.TimeSlipCreatedOn = DateTime.UtcNow; // VNO — mirrors GB4 VnoGenerationDLL.GetNextVno(BizTransactionTypeId, Date) // Returns null when NOGenerationType == 1 (manual numbering) string? vno = await _voucherNumberService.GetNextVoucherNumberAsync( dto.BizTransactionTypeId, dto.TimeSlipDate, login, tx); if (vno != null) dto.TimeSlipNumber = vno; } // DAL returns rows affected — check success, then return the PK // so EventHandler sets ctx.ObjectId = TimeSlipId (used by workflow + outbox) int rows = isNew ? await _TimeSlipDAL.SaveTimeSlip(dto, login, tx) : await _TimeSlipDAL.UpdateTimeSlip(dto, login, tx); if (rows <= 0) throw new Exception(isNew ? "Failed to save TimeSlip." : "Failed to update TimeSlip."); return dto.TimeSlipId; }, new Dictionary(), BIZTRANSACTIONCLASSCONSTANT.TIMESLIP, BizTransactionConstant.TIMESLIP, trans, isNewEntity: isNew ); await _QueryExecutor.CommitAsync(trans); string typeLabel = dto.TimeSlipType switch { 0 => "Onduty", // OnDuty → Permission (as per your requirement) 1 => "Permission", _ => "TimeSlip" }; return isNew ? $"{typeLabel} Request Saved Successfully with Number: {dto.TimeSlipNumber}" : $"{typeLabel} Request Updated Successfully with Number: {dto.TimeSlipNumber}"; } catch { await _QueryExecutor.RollbackAsync(trans); if (autoNumber != null && dto.TimeSlipId > 0) await _AutoNumber.RollbackAutoNumber("TIMESLIP", dto.TimeSlipId, login); throw; } } catch (ArgumentException vex) { // Re-wrap so the caller receives a plain Exception with the validation message throw new Exception(vex.Message, vex); } catch (Exception) { throw; } } /* ══════════════════════════════════════════════════════════════ PRIVATE HELPERS ══════════════════════════════════════════════════════════════ */ private static void ValidateTimeSlip(TimeSlipDTO dto) { if (dto.TimeSlipTimeSlipStartTime <= 0) throw new ArgumentException("Start Time is required"); if (dto.TimeSlipTimeSlipEndTime <= 0) throw new ArgumentException("End Time is required"); if (dto.TimeSlipTimeSlipStartTime >= dto.TimeSlipTimeSlipEndTime) throw new ArgumentException("Start Time must be less than End Time"); } /// Converts integer minutes to "HH:mm" string — mirrors GB4 MinutesIntoTime. private static string MinutesIntoTime(int totalMinutes) { if (totalMinutes <= 0) return "00:00"; return $"{totalMinutes / 60:D2}:{totalMinutes % 60:D2}"; } /// Accepts "/Date(ms)/" Unix-ms strings OR standard date strings. private static DateTime ConvertToDate(object date) { if (date == null) return DateTime.Now; string str = date.ToString()!.Trim(); if (str.StartsWith("/Date(", StringComparison.Ordinal)) { int start = str.IndexOf('(') + 1; int end = str.IndexOfAny(new[] { '+', '-', ')' }, start); long ms = long.Parse(str.Substring(start, end - start)); return DateTimeOffset.FromUnixTimeMilliseconds(ms).UtcDateTime; } return Convert.ToDateTime(str); } public async Task GetSelectListTimeSlip(int FirstNumber, int MaxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _TimeSlipDAL.GetSelectListTimeSlip(FirstNumber, MaxResult, criteriaDTO, loginDTO); } catch (Exception) { throw; } } public async Task GetTimeSlipSummary(CriteriaDTO criteriaDTO, LoginDTO LoginDTO) { try { var result = await _TimeSlipDAL.GetTimeSlipSummary(criteriaDTO, LoginDTO); return JsonConvert.SerializeObject(result); } catch (Exception) { throw; } } public async Task DeleteTimeSlip(int TimeSlipId, LoginDTO LoginDTO) { try { // 🔹 STEP 1: Get record before delete var existing = await _TimeSlipDAL.GetTimeSlipBasic(TimeSlipId, LoginDTO); if (existing == null) return $"{ErrorResponse.DeleteNotFoundMessage}"; // 🔹 STEP 2: Delete string Sql = TimeSlipQB.DELETE_TIMESLIP; var parameter = new { timeslipid = TimeSlipId }; var result = await _QueryExecutor.ExecuteAsync(LoginDTO, Sql, parameter); if (result > 0) { string typeLabel = existing.TimeSlipType switch { 0 => "Onduty", 1 => "Permission", _ => "TimeSlip" }; return $"{typeLabel} Request Deleted Successfully with the Number: {existing.TimeSlipNumber}"; } else { return $"{ErrorResponse.DeleteNotFoundMessage}"; } } catch (Exception ex) { string Error = await _Validation.HandleException(ex, $"{ErrorResponse.DeleteErrorMessage}"); throw new Exception(Error); } } public async Task TimeSlip(int FirstNumber, int MaxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _TimeSlipDAL.TimeSlip(FirstNumber, MaxResult, criteriaDTO, loginDTO); } catch (Exception) { throw; } } public async Task SelectList(int FirstNumber, int MaxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _TimeSlipDAL.SelectList(FirstNumber, MaxResult, criteriaDTO, loginDTO); } catch (Exception) { throw; } } } }