using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using BMA.EHR.Recruit.Service.Data; using BMA.EHR.Recruit.Service.Models.Recruits; using BMA.EHR.Recruit.Service.Core; using BMA.EHR.MetaData.Service.Models; using BMA.EHR.Domain.Models.Placement; using BMA.EHR.Recurit.Service.Data; using System.Security.Claims; using System.Net.Http.Headers; using Newtonsoft.Json; using System.Globalization; using BMA.EHR.Recruit.Service.Requests.Recruits; namespace BMA.EHR.Recruit.Service.Services { public class RecruitService { private readonly ApplicationDbContext _context; private readonly MetadataDbContext _contextMetadata; private readonly OrgDbContext _contextOrg; private readonly MinIOService _minIOService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IConfiguration _configuration; public RecruitService(ApplicationDbContext context, MetadataDbContext contextMetadata, OrgDbContext contextOrg, IHttpContextAccessor httpContextAccessor, MinIOService minIOService, IConfiguration configuration) { _context = context; _contextMetadata = contextMetadata; _contextOrg = contextOrg; _minIOService = minIOService; _httpContextAccessor = httpContextAccessor; _configuration = configuration; } #region " Properties " private string? UserId => _httpContextAccessor?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; private string? FullName => _httpContextAccessor?.HttpContext?.User?.FindFirst("name")?.Value; private string? token => _httpContextAccessor?.HttpContext?.Request.Headers["Authorization"]; #endregion public int GetExamCount(string citizenId) { try { var count = _context.Recruits.AsQueryable() .Where(x => x.CitizenId == citizenId) .Count(); return count; } catch { throw; } } public async Task GetExamAttributeAsync(Guid period, Guid exam) { try { var payment = await _context.RecruitPayments.AsQueryable() .Include(x => x.Recruit) .ThenInclude(x => x.RecruitImport) .Where(x => x.Recruit.Id == exam) .Where(x => x.Recruit.RecruitImport.Id == period) .FirstOrDefaultAsync(); return payment != null ? "มีคุณสมบัติ" : "ไม่มีคุณสมบัติ"; } catch { throw; } } public bool CheckValidCertificate(DateTime certDate, int nextYear = 5) { var valid = true; if (DateTime.Now.Date > certDate.Date.AddYears(nextYear)) valid = false; return valid; } public async Task UpdateDocAsync(Guid ImportId, IFormFileCollection files) { var periodExam = await _context.RecruitImports.AsQueryable() .FirstOrDefaultAsync(x => x.Id == ImportId); if (periodExam == null) throw new Exception(GlobalMessages.DataNotFound); foreach (var file in files) { var doc = await _minIOService.UploadFileAsync(file); var periodExamDocument = new RecruitImportDocument { RecruitImportId = ImportId, DocumentId = doc.Id, }; await _context.RecruitImportDocuments.AddAsync(periodExamDocument); } await _context.SaveChangesAsync(); } public async Task UpdateImageAsync(Guid ImportId, IFormFileCollection files) { var periodExam = await _context.RecruitImports.AsQueryable() .FirstOrDefaultAsync(x => x.Id == ImportId); if (periodExam == null) throw new Exception(GlobalMessages.DataNotFound); foreach (var file in files) { var doc = await _minIOService.UploadFileAsync(file); var periodExamImage = new RecruitImportImage { RecruitImportId = ImportId, DocumentId = doc.Id, }; await _context.RecruitImportImages.AddAsync(periodExamImage); } await _context.SaveChangesAsync(); } public async Task DeleteImageAsync(Guid id) { var image = await _context.RecruitImportImages.AsQueryable() .Include(x => x.Document) .FirstOrDefaultAsync(x => x.Id == id); if (image == null) throw new Exception(GlobalMessages.DataNotFound); var doc_id = image.Document.Id; _context.RecruitImportImages.Remove(image); await _context.SaveChangesAsync(); await _minIOService.DeleteFileAsync(doc_id); } public async Task DeleteDocAsync(Guid id) { var doc = await _context.RecruitImportDocuments.AsQueryable() .Include(x => x.Document) .FirstOrDefaultAsync(x => x.Id == id); if (doc == null) throw new Exception(GlobalMessages.DataNotFound); var doc_id = doc.Document.Id; _context.RecruitImportDocuments.Remove(doc); await _context.SaveChangesAsync(); await _minIOService.DeleteFileAsync(doc_id); } public async Task UpdateAsyncRecruitToPlacement(Guid examId, DateTime accountStartDate) { try { // 🚀 Prepare HTTP client once var httpClient1 = new HttpClient(); httpClient1.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); httpClient1.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); var apiUrl1 = $"{_configuration["API"]}/org/pos/level"; var response1 = await httpClient1.GetStringAsync(apiUrl1); var posOptions = JsonConvert.DeserializeObject(response1); var recruitImport = await _context.RecruitImports.AsQueryable() .FirstOrDefaultAsync(x => x.Id == examId); if (recruitImport == null) throw new Exception(GlobalMessages.DataNotFound); var _placement = await _contextMetadata.Placements.AsQueryable() .FirstOrDefaultAsync(x => x.PlacementType.Name == "สอบแข่งขัน" && x.RefId == recruitImport.Id); if (_placement != null) throw new Exception("รอบการสอบนี้ได้ทำการบรรจุไปแล้ว"); // 🚀 Pre-load all lookup data once var placementTypesCache = await _contextMetadata.PlacementTypes.ToListAsync(); var provincesCache = await _contextOrg.province.ToListAsync(); var districtsCache = await _contextOrg.district.ToListAsync(); var subDistrictsCache = await _contextOrg.subDistrict.ToListAsync(); var educationLevelsCache = await _contextOrg.educationLevel.ToListAsync(); var placement = new Placement { Name = recruitImport.Name, RefId = recruitImport.Id, Round = recruitImport.Order.ToString() ?? "", Year = recruitImport.Year, Number = await _context.Recruits.AsQueryable().Where(x => x.RecruitImport == recruitImport).CountAsync(), PlacementType = placementTypesCache.FirstOrDefault(x => x.Name.Trim().ToUpper().Contains("สอบแข่งขัน")) ?? placementTypesCache.First(), StartDate = accountStartDate, EndDate = accountStartDate.AddYears(2).AddDays(-1), CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", CreatedFullName = FullName ?? "", LastUpdatedAt = DateTime.Now, LastUpdateUserId = UserId ?? "", LastUpdateFullName = FullName ?? "", }; await _contextMetadata.Placements.AddAsync(placement); // 🚀 Load all related data with single queries var candidates = await _context.Recruits.AsQueryable() .Include(x => x.Addresses) .Include(x => x.Certificates) .Include(x => x.Educations) .Include(x => x.Occupations) .Where(x => x.RecruitImport == recruitImport) .ToListAsync(); var scoreImport = await _context.ScoreImports.AsQueryable() .FirstOrDefaultAsync(x => x.RecruitImport == recruitImport); var recruitScores = await _context.RecruitScores.AsQueryable() .Where(x => x.ScoreImport == scoreImport && x.ExamStatus == "ผ่าน") .ToListAsync(); var recruitScoresDict = recruitScores .Where(x => !string.IsNullOrWhiteSpace(x.ExamId)) .ToDictionary(x => x.ExamId, x => x); // 🚀 Prepare HTTP client once var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); httpClient.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); // 🚀 Batch HTTP requests var orgTasks = candidates.Select(async candidate => { if (string.IsNullOrWhiteSpace(candidate.CitizenId)) return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null }; var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}"; try { var response = await httpClient.GetStringAsync(apiUrl); return new { CitizenId = candidate.CitizenId, org = JsonConvert.DeserializeObject(response) }; } catch { return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null }; } }).ToList(); var orgResults = await Task.WhenAll(orgTasks); var orgDict = orgResults.ToDictionary(x => x.CitizenId ?? "", x => x.org); // 🚀 Prepare batch inserts var placementProfiles = new List(); var placementEducations = new List(); var placementCertificates = new List(); foreach (var candidate in candidates) { if (string.IsNullOrWhiteSpace(candidate.ExamId) || !recruitScoresDict.TryGetValue(candidate.ExamId, out var recruitScore)) continue; var org = orgDict.TryGetValue(candidate.CitizenId ?? "", out var orgValue) ? orgValue : null; var isOfficer = org?.result != null; // 🚀 Cache repeated calculations var firstAddress = candidate.Addresses?.FirstOrDefault(); var firstEducation = candidate.Educations?.FirstOrDefault(); var firstCertificate = candidate.Certificates?.FirstOrDefault(); var firstOccupation = candidate.Occupations?.FirstOrDefault(); var registAddress = BuildAddress(firstAddress?.Address, firstAddress?.Moo, firstAddress?.Soi, firstAddress?.Road); var currentAddress = BuildAddress(firstAddress?.Address1, firstAddress?.Moo1, firstAddress?.Soi1, firstAddress?.Road1); // หาค่า posLevelName หลังสุด var posLevelObject = posOptions?.result?.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.posLevelName) && !string.IsNullOrWhiteSpace(candidate.PositionName) && candidate.PositionName.Contains(x.posLevelName)); // เก็บเฉพาะค่า posLevelName var posLevelName = posLevelObject?.posLevelName; // สร้างตัวแปร PositionName ที่ตัดค่า posLevelName ออก var positionNameWithoutLevel = candidate.PositionName ?? ""; if (!string.IsNullOrWhiteSpace(posLevelName)) { positionNameWithoutLevel = positionNameWithoutLevel.Replace(posLevelName, "").Trim(); } var placementProfile = new PlacementProfile { Placement = placement, PositionCandidate = positionNameWithoutLevel ?? "", PositionType = posLevelObject?.posTypes?.posTypeName ?? "", PositionLevel = posLevelName ?? "", Prefix = candidate.Prefix ?? "", Firstname = candidate.FirstName ?? "", Lastname = candidate.LastName ?? "", Gender = candidate.Gendor ?? "", Nationality = candidate.National ?? "", Race = candidate.Race ?? "", Religion = candidate.Religion ?? "", DateOfBirth = candidate.DateOfBirth, Relationship = candidate.Marry ?? "", CitizenId = candidate.CitizenId ?? "", CitizenProvinceId = provincesCache.FirstOrDefault(x => x.name == candidate.CitizenCardIssuer)?.Id, CitizenDate = candidate.CitizenCardExpireDate, Telephone = firstAddress?.Telephone ?? "", MobilePhone = firstAddress?.Mobile ?? "", RegistAddress = registAddress ?? "", RegistProvinceId = provincesCache.FirstOrDefault(x => x.name == firstAddress?.Province)?.Id, RegistDistrictId = districtsCache.FirstOrDefault(x => x.name == firstAddress?.Amphur)?.Id, RegistSubDistrictId = subDistrictsCache.FirstOrDefault(x => x.name == firstAddress?.District)?.Id, RegistZipCode = firstAddress?.ZipCode ?? "", RegistSame = false, CurrentAddress = currentAddress, CurrentProvinceId = provincesCache.FirstOrDefault(x => x.name == firstAddress?.Province1)?.Id, CurrentDistrictId = districtsCache.FirstOrDefault(x => x.name == firstAddress?.Amphur1)?.Id, CurrentSubDistrictId = subDistrictsCache.FirstOrDefault(x => x.name == firstAddress?.District1)?.Id, CurrentZipCode = firstAddress?.ZipCode1, Marry = candidate.Marry?.Contains("สมรส") ?? false, OccupationPositionType = "other", OccupationTelephone = firstOccupation?.Telephone ?? "", OccupationPosition = firstOccupation?.Position ?? "", PointTotalA = recruitScore.FullA, // non-nullable int PointA = recruitScore.SumA, // non-nullable double PointTotalB = recruitScore.FullB ?? 0, // nullable int? PointB = recruitScore.SumB ?? 0, // nullable double? PointTotalC = recruitScore.FullC, // non-nullable int PointC = recruitScore.SumC, // non-nullable double ExamNumber = !string.IsNullOrWhiteSpace(recruitScore.Number) && int.TryParse(recruitScore.Number, out int n) ? n : null, ExamRound = null, IsRelief = false, PlacementStatus = "UN-CONTAIN", Pass = recruitScore.ExamStatus ?? "", RemarkHorizontal = "โดยมีเงื่อนไขว่าต้องปฏิบัติงานให้กรุงเทพมหานครเป็นระยะเวลาไม่น้อยกว่า ๕ ปี นับแต่วันที่ได้รับการบรรจุและแต่งตั้ง โดยห้ามโอนไปหน่วยงานหรือส่วนราชการอื่น เว้นเเต่ลาออกจากราชการ", Amount = org?.result?.amount, PositionSalaryAmount = org?.result?.positionSalaryAmount, MouthSalaryAmount = org?.result?.mouthSalaryAmount, CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", CreatedFullName = FullName ?? "", LastUpdatedAt = DateTime.Now, LastUpdateUserId = UserId ?? "", LastUpdateFullName = FullName ?? "", IsOfficer = isOfficer, profileId = org?.result?.profileId ?? "", IsOld = org?.result != null, AmountOld = org?.result?.amount, nodeOld = org?.result?.node ?? "", nodeIdOld = org?.result?.nodeId ?? "", posmasterIdOld = org?.result?.posmasterId ?? "", rootOld = org?.result?.root ?? "", rootIdOld = org?.result?.rootId ?? "", rootShortNameOld = org?.result?.rootShortName ?? "", child1Old = org?.result?.child1 ?? "", child1IdOld = org?.result?.child1Id ?? "", child1ShortNameOld = org?.result?.child1ShortName ?? "", child2Old = org?.result?.child2 ?? "", child2IdOld = org?.result?.child2Id ?? "", child2ShortNameOld = org?.result?.child2ShortName ?? "", child3Old = org?.result?.child3 ?? "", child3IdOld = org?.result?.child3Id ?? "", child3ShortNameOld = org?.result?.child3ShortName ?? "", child4Old = org?.result?.child4 ?? "", child4IdOld = org?.result?.child4Id ?? "", child4ShortNameOld = org?.result?.child4ShortName ?? "", orgRevisionIdOld = org?.result?.orgRevisionId ?? "", posMasterNoOld = org?.result?.posMasterNo, positionNameOld = org?.result?.position ?? "", posTypeIdOld = org?.result?.posTypeId ?? "", posTypeNameOld = org?.result?.posTypeName ?? "", posLevelIdOld = org?.result?.posLevelId ?? "", posLevelNameOld = org?.result?.posLevelName ?? "", }; placementProfiles.Add(placementProfile); var placementEducation = new PlacementEducation { PlacementProfile = placementProfile, EducationLevelId = educationLevelsCache.FirstOrDefault(x => x.name == firstEducation?.HighDegree)?.Id, EducationLevelName = educationLevelsCache.FirstOrDefault(x => x.name == firstEducation?.HighDegree)?.name, Field = firstEducation?.Major ?? "", Gpa = firstEducation == null || firstEducation?.GPA == null ? "" : firstEducation.GPA.ToString(), Institute = firstEducation?.University ?? "", Degree = firstEducation?.Degree ?? "", FinishDate = firstEducation?.BachelorDate, IsDate = true, CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", LastUpdatedAt = DateTime.Now, LastUpdateUserId = UserId ?? "", CreatedFullName = FullName ?? "", LastUpdateFullName = FullName ?? "", }; placementEducations.Add(placementEducation); var placementCertificate = new PlacementCertificate { PlacementProfile = placementProfile, CertificateNo = firstCertificate?.CertificateNo ?? "", IssueDate = firstCertificate?.IssueDate, ExpireDate = firstCertificate?.ExpiredDate, CertificateType = firstCertificate?.Description ?? "", CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", LastUpdatedAt = DateTime.Now, LastUpdateUserId = UserId ?? "", CreatedFullName = FullName ?? "", LastUpdateFullName = FullName ?? "", }; placementCertificates.Add(placementCertificate); } // 🚀 Batch insert all records await _contextMetadata.PlacementProfiles.AddRangeAsync(placementProfiles); await _contextMetadata.PlacementEducations.AddRangeAsync(placementEducations); await _contextMetadata.PlacementCertificates.AddRangeAsync(placementCertificates); // 🚀 Single SaveChanges at the end await _contextMetadata.SaveChangesAsync(); httpClient.Dispose(); } catch { throw; } } private string BuildAddress(string? address, string? moo, string? soi, string? road) { var parts = new List(); if (!string.IsNullOrWhiteSpace(address)) parts.Add(address); if (!string.IsNullOrWhiteSpace(moo)) parts.Add($"หมู่ {moo}"); if (!string.IsNullOrWhiteSpace(soi)) parts.Add($"ซอย {soi}"); if (!string.IsNullOrWhiteSpace(road)) parts.Add($"ถนน {road}"); return string.Join(" ", parts); } public DateTime CheckDateTime(string Date, string Formate) { // ตอนนี้ทำไว้ให้รองรับแค่ "dd/MM/yyyy", "yyyy-MM-dd" Date = Date.Trim(); if (string.IsNullOrWhiteSpace(Date)) return DateTime.MinValue; // จะเข้าเฉพาะกรณีที่ string เป็นตัวเลข เช่น "35635", "44561.5" if (double.TryParse(Date, out double oaDate)) { try { Date = DateTime.FromOADate(oaDate).ToString(Formate); } catch { Date = DateTime.MinValue.ToString(Formate); } } string[] parts = Date.Trim().Replace("-", "/").Split("/"); if (parts.Length != 3) return DateTime.MinValue; int year; int month; int day; switch (Formate) { case "dd/MM/yyyy": if (int.TryParse(parts[2], out year) && year > 2500) { year -= 543; } else if (!int.TryParse(parts[2], out year)) { return DateTime.MinValue; } if (!int.TryParse(parts[1], out month)) return DateTime.MinValue; if (!int.TryParse(parts[0], out day)) return DateTime.MinValue; break; case "yyyy-MM-dd": if (int.TryParse(parts[0], out year) && year > 2500) { year -= 543; } else if (!int.TryParse(parts[0], out year)) { return DateTime.MinValue; } if (!int.TryParse(parts[1], out month)) return DateTime.MinValue; if (!int.TryParse(parts[2], out day)) return DateTime.MinValue; break; default: return DateTime.MinValue; } if (month < 1 || month > 12) month = 1; int maxDay = DateTime.DaysInMonth(year, month); if (day < 1) day = 1; else if (day > maxDay) day = maxDay; var normalDate = $"{day}/{(month >= 1 && month <= 9 ? $"0{month}" : month)}/{year}"; if (DateTime.TryParseExact(normalDate, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsedDate)) { return parsedDate; } return DateTime.MinValue; } } }