hrms-api-backend/BMA.EHR.Application/Repositories/Leaves/LeaveRequests/LeaveBeginingRepository.cs

582 lines
22 KiB
C#
Raw Normal View History

using Amazon.S3.Model;
using BMA.EHR.Application.Common.Interfaces;
2025-04-23 11:25:12 +07:00
using BMA.EHR.Application.Messaging;
2025-10-09 22:16:19 +07:00
using BMA.EHR.Application.Responses.Profiles;
using BMA.EHR.Domain.Extensions;
using BMA.EHR.Domain.Models.Leave.Commons;
2025-04-23 11:25:12 +07:00
using BMA.EHR.Domain.Models.Leave.Requests;
using BMA.EHR.Domain.Shared;
2025-04-23 11:25:12 +07:00
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
2026-06-19 10:51:18 +07:00
using System.Collections.Concurrent;
2025-04-23 11:25:12 +07:00
namespace BMA.EHR.Application.Repositories.Leaves.LeaveRequests
{
public class LeaveBeginningRepository : GenericLeaveRepository<Guid, LeaveBeginning>
{
#region " Fields "
private readonly ILeaveDbContext _dbContext;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly OrganizationCommonRepository _organizationCommonRepository;
private readonly UserProfileRepository _userProfileRepository;
private readonly IConfiguration _configuration;
private readonly EmailSenderService _emailSenderService;
2026-06-19 10:51:18 +07:00
/// <summary>
/// Keyed locks to serialize get-or-create for LeaveBeginning rows by (ProfileId, LeaveYear, LeaveTypeId).
/// Prevents duplicate inserts when concurrent requests (e.g. UI calling /user/check twice) hit the same key.
/// </summary>
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _getOrAddLocks = new();
2025-04-23 11:25:12 +07:00
#endregion
#region " Constructor and Destuctor "
public LeaveBeginningRepository(ILeaveDbContext dbContext,
IHttpContextAccessor httpContextAccessor,
OrganizationCommonRepository organizationCommonRepository,
UserProfileRepository userProfileRepository,
IConfiguration configuration,
EmailSenderService emailSenderService) : base(dbContext, httpContextAccessor)
{
_dbContext = dbContext;
_httpContextAccessor = httpContextAccessor;
_organizationCommonRepository = organizationCommonRepository;
_userProfileRepository = userProfileRepository;
_configuration = configuration;
_emailSenderService = emailSenderService;
}
#endregion
#region " Properties "
protected Guid UserOrganizationId
{
get
{
if (UserId != null || UserId != "")
return _userProfileRepository.GetUserOCId(Guid.Parse(UserId!), AccessToken);
else
return Guid.Empty;
}
}
#endregion
public async Task<List<LeaveBeginning>> GetAllByYearAsync(int year)
{
return await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
2025-04-23 11:25:12 +07:00
.Where(x => x.LeaveYear == year)
.ToListAsync();
}
public async Task<LeaveBeginning?> GetByYearAndTypeIdAsync(int year, Guid typeId)
{
var data = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId);
return data;
}
public async Task UpdateLeaveUsageAsync(int year, Guid typeId, Guid userId, double day)
{
2026-01-29 13:37:38 +07:00
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var data = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
2025-09-02 14:55:07 +07:00
if (data == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
data.LeaveDaysUsed += day;
await _dbContext.SaveChangesAsync();
}
public async Task UpdateLeaveCountAsync(int year, Guid typeId, Guid userId, int count)
{
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var data = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
if (data == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
data.LeaveCount += count;
await _dbContext.SaveChangesAsync();
}
2026-06-19 10:51:18 +07:00
public async Task ProcessEarlyLeaveRequest(int year)
{
2026-06-19 11:50:36 +07:00
// Get Early Leave Request (กรองตามปีงบประมาณ: 1 ต.ค. (year-1) 30 ก.ย. (year))
var fiscalStart = new DateTime(year - 1, 10, 1);
var fiscalEnd = new DateTime(year, 9, 30);
2026-06-19 10:51:18 +07:00
var leaveReq = await _dbContext.Set<LeaveRequest>()
.Include(x => x.Type)
.Where(x => x.LeaveStatus == "APPROVE")
2026-06-19 11:50:36 +07:00
.Where(x => x.LeaveStartDate.Date <= fiscalEnd && x.LeaveEndDate.Date >= fiscalStart)
2026-06-19 10:51:18 +07:00
.ToListAsync();
foreach (var leave in leaveReq)
{
2026-06-19 11:50:36 +07:00
await GetByYearAndTypeIdForUserWithUpdateAsync(year, leave.Type.Id, leave.KeycloakUserId);
2026-06-19 10:51:18 +07:00
}
}
public async Task ProcessEarlyLeaveRequestSchedule()
{
int year = DateTime.Now.Year;
await ProcessEarlyLeaveRequest(year);
}
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUserAsync(int year, Guid typeId, Guid userId)
{
2026-01-29 13:37:38 +07:00
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
2026-06-19 10:51:18 +07:00
LeaveBeginning Factory()
{
var limit = 0.0;
2026-06-19 10:51:18 +07:00
var prev = _dbContext.Set<LeaveBeginning>()
2025-10-09 22:16:19 +07:00
.Include(x => x.LeaveType)
2026-06-19 10:51:18 +07:00
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
2026-06-22 10:18:21 +07:00
// คำนวณปีงบประมาณจาก startDate (ปีงบประมาณเริ่ม 1 ต.ค. และสิ้นสุด 30 ก.ย.)
var isCurrentYear = DateTime.Now.Year == year;
2025-10-09 22:16:19 +07:00
var prevRemain = 0.0;
if (prev != null)
{
2026-06-22 10:18:21 +07:00
prevRemain = isCurrentYear ? prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0) : 0.0;
2025-10-09 22:16:19 +07:00
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
2026-06-19 10:51:18 +07:00
return new LeaveBeginning
2025-10-09 22:16:19 +07:00
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = pf.RootDnaId,
Child1DnaId = pf.Child1DnaId,
Child2DnaId = pf.Child2DnaId,
Child3DnaId = pf.Child3DnaId,
Child4DnaId = pf.Child4DnaId
2025-10-09 22:16:19 +07:00
};
}
2026-06-19 10:51:18 +07:00
return await GetOrAddForUserAsync(year, typeId, pf.Id, Factory);
2025-10-09 22:16:19 +07:00
}
2026-06-19 11:50:36 +07:00
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUserWithUpdateAsync(int year, Guid typeId, Guid userId)
{
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
var limit = 0.0;
var prev = _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
var prevRemain = 0.0;
2026-06-22 10:18:21 +07:00
2026-06-19 11:50:36 +07:00
if (prev != null)
{
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
var data = await _dbContext.Set<LeaveBeginning>()
.Where(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == pf.Id)
.FirstOrDefaultAsync();
if (data != null)
{
data.LeaveDays = leaveType?.Code == "LV-005" ? limit : 0;
await _dbContext.SaveChangesAsync();
}
// return new LeaveBeginning
// {
// LeaveYear = year,
// LeaveTypeId = typeId,
// ProfileId = pf.Id,
// Prefix = pf.Prefix,
// FirstName = pf.FirstName,
// LastName = pf.LastName,
// LeaveDaysUsed = 0,
// LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
// RootDnaId = pf.RootDnaId,
// Child1DnaId = pf.Child1DnaId,
// Child2DnaId = pf.Child2DnaId,
// Child3DnaId = pf.Child3DnaId,
// Child4DnaId = pf.Child4DnaId
// };
return data;
}
2025-10-09 22:16:19 +07:00
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUser(int year, Guid typeId, GetProfileByKeycloakIdDto? pf)
{
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
2025-10-09 22:16:19 +07:00
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
2026-06-19 10:51:18 +07:00
LeaveBeginning Factory()
2025-10-09 22:16:19 +07:00
{
var limit = 0.0;
2026-06-19 10:51:18 +07:00
var prev = _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
2026-06-19 10:51:18 +07:00
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
var prevRemain = 0.0;
if (prev != null)
{
2026-05-05 12:37:38 +07:00
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
2026-06-19 10:51:18 +07:00
return new LeaveBeginning
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = pf.RootDnaId,
Child1DnaId = pf.Child1DnaId,
Child2DnaId = pf.Child2DnaId,
Child3DnaId = pf.Child3DnaId,
Child4DnaId = pf.Child4DnaId
};
}
2026-06-19 10:51:18 +07:00
return await GetOrAddForUserAsync(year, typeId, pf.Id, Factory);
}
2025-09-02 14:55:07 +07:00
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUser2Async(int year, Guid typeId, Guid userId)
{
2026-01-29 13:37:38 +07:00
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
2025-09-02 14:55:07 +07:00
if (pf == null)
{
return null;
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
2025-09-02 14:55:07 +07:00
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
2026-06-19 10:51:18 +07:00
LeaveBeginning Factory()
2025-09-02 14:55:07 +07:00
{
var limit = 0.0;
2026-06-19 10:51:18 +07:00
var prev = _dbContext.Set<LeaveBeginning>()
2025-09-02 14:55:07 +07:00
.Include(x => x.LeaveType)
2026-06-19 10:51:18 +07:00
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
2025-09-02 14:55:07 +07:00
var prevRemain = 0.0;
if (prev != null)
{
2026-05-05 12:37:38 +07:00
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
2025-09-02 14:55:07 +07:00
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
2026-06-19 10:51:18 +07:00
return new LeaveBeginning
2025-09-02 14:55:07 +07:00
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = pf.RootDnaId,
Child1DnaId = pf.Child1DnaId,
Child2DnaId = pf.Child2DnaId,
Child3DnaId = pf.Child3DnaId,
Child4DnaId = pf.Child4DnaId
2025-09-02 14:55:07 +07:00
};
}
2026-06-19 10:51:18 +07:00
return await GetOrAddForUserAsync(year, typeId, pf.Id, Factory);
}
/// <summary>
/// Get-or-create a LeaveBeginning row for (ProfileId, LeaveYear, LeaveTypeId) with concurrency protection.
/// Uses a keyed SemaphoreSlim to serialize within-process requests, and re-queries after acquiring the lock.
/// If a cross-process insert wins (unique index violation), the duplicate key exception is caught and the row
/// created by the winner is returned.
/// </summary>
private async Task<LeaveBeginning?> GetOrAddForUserAsync(int year, Guid typeId, Guid profileId, Func<LeaveBeginning> factory)
{
var key = $"{profileId}_{year}_{typeId}";
var semaphore = _getOrAddLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
await semaphore.WaitAsync();
try
{
// Re-query inside the lock — another thread may have created it while we waited.
var existing = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == profileId);
if (existing != null)
{
return existing;
}
var entity = factory();
_dbContext.Set<LeaveBeginning>().Add(entity);
try
{
await _dbContext.SaveChangesAsync();
return entity;
}
catch (DbUpdateException)
{
// Cross-process/cross-server race hit the unique index (IX_LeaveBeginnings_ProfileId_LeaveYear_LeaveTypeId).
// Detach the failed insert and return the row created by the winner.
_dbContext.Detach(entity);
var winner = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == profileId);
return winner;
}
}
finally
{
semaphore.Release();
}
2025-09-02 14:55:07 +07:00
}
public async Task<List<LeaveBeginning>> GetAllByYearAndTypeAsync(int year, Guid typeId, List<ProfileData> userIdList)
{
2025-09-02 14:55:07 +07:00
var updateList = new List<LeaveBeginning>();
var result = new List<LeaveBeginning>();
var beginningList = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.Where(x => x.LeaveYear == year && x.LeaveTypeId == typeId)
.ToListAsync();
foreach (var pf in userIdList)
{
//var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(id, AccessToken);
//if (pf == null)
//{
// continue; // Goto Next Id
//}
var profile = await _userProfileRepository.GetProfileByProfileIdAsync(pf.Id, AccessToken);
if (profile == null)
{
return null;
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
2025-09-02 14:55:07 +07:00
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
var data = beginningList.FirstOrDefault(x => x.ProfileId == pf.Id);
if (data == null)
{
var limit = 0.0;
var prev = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
var prevRemain = 0.0;
if (prev != null)
{
2026-05-05 12:37:38 +07:00
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
2025-09-02 14:55:07 +07:00
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
data = new LeaveBeginning
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = profile.RootDnaId,
Child1DnaId = profile.Child1DnaId,
Child2DnaId = profile.Child2DnaId,
Child3DnaId = profile.Child3DnaId,
Child4DnaId = profile.Child4DnaId
2025-09-02 14:55:07 +07:00
};
updateList.Add(data);
}
result.Add(data);
}
if (!updateList.Any())
{
await _dbContext.Set<LeaveBeginning>().AddRangeAsync(updateList);
await _dbContext.SaveChangesAsync();
}
return result;
}
}
public class ProfileData
{
public Guid Id { get; set; } = Guid.Empty;
public string Prefix { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime? DateStart { get; set; } = null;
public DateTime? DateAppoint { get; set; } = null;
2025-04-23 11:25:12 +07:00
}
}