diff --git a/.github/workflows/discord-notify.yml b/.github/workflows/discord-notify.yml new file mode 100644 index 0000000..ce4ee51 --- /dev/null +++ b/.github/workflows/discord-notify.yml @@ -0,0 +1,22 @@ +name: Discord PR Notify + +on: + pull_request: + types: [opened] + +jobs: + discord: + runs-on: ubuntu-latest + steps: + - name: Send Discord + run: | + curl -X POST "${{ secrets.DISCORD_WEBHOOK_PULLREQUEST }}" \ + -H "Content-Type: application/json" \ + -d '{ + "embeds": [{ + "title": "🔔 **Service:** ${{ github.repository }}", + "description": "ðŸ‘Ī **Author:** ${{ github.event.pull_request.user.login }}\nðŸŒŋ **Branch:** ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}\nðŸ“Ķ **Pull Request:** [#${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }})", + "color": 5814783, + "timestamp": "${{ github.event.pull_request.created_at }}" + }] + }' diff --git a/Controllers/DisableController.cs b/Controllers/DisableController.cs index c8271fe..8ff0ff8 100644 --- a/Controllers/DisableController.cs +++ b/Controllers/DisableController.cs @@ -27,6 +27,7 @@ using System.Text; using Newtonsoft.Json.Linq; using Newtonsoft.Json; using System.Net.Http.Headers; +using BMA.EHR.Recurit.Exam.Service.Request; namespace BMA.EHR.Recurit.Exam.Service.Controllers { @@ -2013,108 +2014,92 @@ namespace BMA.EHR.Recurit.Exam.Service.Controllers if (periodExam == null) return Error(GlobalMessages.DataNotFound, StatusCodes.Status404NotFound); - var data = new List(); - var p_Id = new MySqlParameter("@id", id); - int total = 0; + var query = _context.Disables + .Include(x => x.PeriodExam) + .Include(x => x.Educations) + .Include(x => x.Certificates) + .OrderBy(x => x.ExamId) + .Where(x => x.PeriodExam != null && x.PeriodExam.Id == id); - // --------------------------- - // 1ïļ. āļ”āļķāļ‡āļĢāļēāļĒāļĨāļ°āđ€āļ­āļĩāļĒāļ”āļŠāļ­āļš (exam_info) - // --------------------------- - using (var cmd = _context.Database.GetDbConnection().CreateCommand()) + var keywordParam = req.keyword?.Trim(); + if (!string.IsNullOrWhiteSpace(keywordParam)) { - cmd.CommandTimeout = 0; - - var sb = new StringBuilder(); - sb.Append(@" - SELECT - examID, profileID, prefix, fullName, dateofbirth, gender, degree, major, majorgroup, - certificateno, certificateIssueDate, score, result, examAttribute, remark, isspecial, - applydate, university, position_name, hddPosition, typeTest, position_level, position_type, - exam_name, exam_order, score_year, - COUNT(*) OVER() AS total_count - FROM exam_info - WHERE disable_import_id = @id - "); - - cmd.Parameters.Clear(); - cmd.Parameters.Add(p_Id); - - var keywordParam = req.keyword?.Trim(); - if (!string.IsNullOrWhiteSpace(keywordParam)) - { - sb.Append(@" - AND ( - examID LIKE @kw - OR profileID LIKE @kw - OR prefix LIKE @kw - OR fullName LIKE @kw - OR hddPosition LIKE @kw - OR position_name LIKE @kw - ) - "); - cmd.Parameters.Add(new MySqlParameter("@kw", $"%{keywordParam}%")); - } - - // --------------------------- - // Paging + Sorting - // --------------------------- - sb.Append(" ORDER BY examID "); - sb.Append(" LIMIT @PageSize OFFSET @Offset "); - cmd.Parameters.Add(new MySqlParameter("@PageSize", req.PageSize)); - cmd.Parameters.Add(new MySqlParameter("@Offset", (req.Page - 1) * req.PageSize)); - - cmd.CommandText = sb.ToString(); - - _context.Database.OpenConnection(); - - // --------------------------- - // āļ”āļķāļ‡āļ‚āđ‰āļ­āļĄāļđāļĨ + total - // --------------------------- - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - if (total == 0) - total = Convert.ToInt32(reader["total_count"]); - - data.Add(new - { - examID = reader["examID"].ToString(), - profileID = reader["profileID"].ToString(), - prefix = reader["prefix"].ToString(), - fullName = reader["fullName"].ToString(), - dateOfBirth = reader["dateofbirth"] == DBNull.Value ? "" : Convert.ToDateTime(reader["dateofbirth"]).ToThaiShortDate(), - gender = reader["gender"].ToString(), - degree = reader["degree"].ToString(), - major = reader["major"].ToString(), - majorgroup = reader["majorgroup"].ToString(), - certificateNo = reader["certificateno"].ToString(), - certificateIssueDate = reader["certificateIssueDate"] == DBNull.Value ? "" : Convert.ToDateTime(reader["certificateIssueDate"]).ToThaiShortDate(), - ExamScore = reader["score"] == DBNull.Value ? 0 : Convert.ToDecimal(reader["score"]), - ExamResult = reader["result"].ToString(), - ExamAttribute = reader["examAttribute"].ToString(), - Remark = reader["remark"].ToString(), - IsSpecial = reader["isspecial"].ToString(), - applyDate = reader["applydate"] == DBNull.Value ? "" : Convert.ToDateTime(reader["applydate"]).ToThaiShortDate(), - university = reader["university"].ToString(), - position_name = reader["position_name"].ToString(), - hddPosition = reader["hddPosition"].ToString(), - typeTest = reader["typeTest"].ToString(), - position_level = reader["position_level"].ToString(), - position_type = reader["position_type"].ToString(), - exam_name = reader["exam_name"].ToString(), - exam_order = reader["exam_order"].ToString(), - score_year = Convert.ToInt32(reader["score_year"]).ToThaiYear().ToString() - }); - } - } + query = query.Where(x => + x.ExamId.Contains(keywordParam) || + x.CitizenId.Contains(keywordParam) || + x.Prefix.Contains(keywordParam) || + x.FirstName.Contains(keywordParam) || + x.LastName.Contains(keywordParam) || + x.HddPosition.Contains(keywordParam) || + x.PositionName.Contains(keywordParam) + ); } + int total = await query.CountAsync(); + + query = query + .Skip((req.Page - 1) * req.PageSize) + .Take(req.PageSize); + + var data = await query + .GroupJoin( + _context.DisableScores.Include(x => x.ScoreImport), + rc => new { rc.PeriodExam!.Id, rc.ExamId }, + sc => new { Id = sc.ScoreImport!.PeriodExamId, sc.ExamId }, + (disable, scores) => new { disable, scores } + ) + .SelectMany( + x => x.scores.DefaultIfEmpty(), + (x, sr) => new + { + examID = x.disable.ExamId, + profileID = x.disable.CitizenId, + prefix = x.disable.Prefix, + fullName = $"{x.disable.FirstName} {x.disable.LastName}", + dateOfBirth = x.disable.DateOfBirth != null && x.disable.DateOfBirth != DateTime.MinValue + ? x.disable.DateOfBirth.ToThaiShortDate() + : "", + gender = x.disable.Gendor, + degree = x.disable.Educations.Any() ? x.disable.Educations.First().Degree : "", + major = x.disable.Educations.Any() ? x.disable.Educations.First().Major : "", + certificateNo = x.disable.Certificates.Any() + ? x.disable.Certificates.First().CertificateNo ?? "" + : "", + certificateIssueDate = x.disable.Certificates.Any() && x.disable.Certificates.First().IssueDate != null && x.disable.Certificates.First().IssueDate != DateTime.MinValue + ? x.disable.Certificates.First().IssueDate.ToThaiShortDate() + : "", + examScore = sr == null ? 0.0 : sr.TotalScore, + examResult = sr == null ? "" : sr.ExamStatus, + examAttribute = x.disable.Certificates.Any() && x.disable.Certificates.First().IssueDate != null + ? _disableService.CheckValidCertificate(x.disable.Certificates.First().IssueDate, 5) + ? "āļĄāļĩāļ„āļļāļ“āļŠāļĄāļšāļąāļ•āļī" : "āđ„āļĄāđˆāļĄāļĩāļ„āļļāļ“āļŠāļĄāļšāļąāļ•āļī" + : "āđ„āļĄāđˆāļĄāļĩāļ„āļļāļ“āļŠāļĄāļšāļąāļ•āļī", + remark = x.disable.Remark, + isSpecial = x.disable.Isspecial == "Y" ? x.disable.Isspecial : "", + applyDate = x.disable.ApplyDate != null && x.disable.ApplyDate != DateTime.MinValue + ? x.disable.ApplyDate.ToThaiShortDate() + : "", + university = x.disable.Educations.Any() ? x.disable.Educations.First().University : "", + position_name = x.disable.PositionName, + hddPosition = x.disable.HddPosition ?? "", + typeTest = x.disable.typeTest ?? "", + position_level = x.disable.PositionLevel ?? "", + position_type = x.disable.PositionType ?? "", + exam_name = x.disable.PeriodExam!.Name, + exam_order = x.disable.PeriodExam != null && x.disable.PeriodExam.Round != null + ? x.disable.PeriodExam.Round.ToString() + : "", + score_year = x.disable.PeriodExam != null && x.disable.PeriodExam.Year != null + ? (x.disable.PeriodExam.Year > 2500 ? x.disable.PeriodExam.Year : x.disable.PeriodExam.Year + 543).ToString() + : "", + }) + .ToListAsync(); + // --------------------------- // 3ïļ. āļ”āļķāļ‡āļŠāļĢāļļāļ›āļ„āļ°āđāļ™āļ™ // --------------------------- dynamic header = null; - int _count = await _context.Disables.Where(x=> x.PeriodExam.Id == id).CountAsync(); + int _count = await _context.Disables.Where(x => x.PeriodExam.Id == id).CountAsync(); if (data.Count > 0) { header = await _context.DisableScores @@ -2448,15 +2433,15 @@ namespace BMA.EHR.Recurit.Exam.Service.Controllers /// āđ€āļĄāļ·āđˆāļ­āđ‚āļ­āļ™āļ„āļ™āļŠāļĢāļĢāļŦāļēāđ„āļ›āļšāļĢāļĢāļˆāļļāļŠāļģāđ€āļĢāđ‡āļˆ /// āđ„āļĄāđˆāđ„āļ”āđ‰ Login āđ€āļ‚āđ‰āļēāļĢāļ°āļšāļš /// āđ€āļĄāļ·āđˆāļ­āđ€āļāļīāļ”āļ‚āđ‰āļ­āļœāļīāļ”āļžāļĨāļēāļ”āđƒāļ™āļāļēāļĢāļ—āļģāļ‡āļēāļ™ - [HttpGet("placement/{examId:length(36)}")] + [HttpPost("placement/{examId:length(36)}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> UpdateAsyncDisableToPlacement(Guid examId) + public async Task> UpdateAsyncDisableToPlacement(Guid examId, [FromBody] RecruitDateRequest req) { try { - await _periodExamService.UpdateAsyncDisableToPlacement(examId); + await _periodExamService.UpdateAsyncDisableToPlacement(examId, req.AccountStartDate); return Success(); } catch (Exception ex) diff --git a/Models/EducationLevel.cs b/Models/EducationLevel.cs index 55f249e..c33cf6c 100644 --- a/Models/EducationLevel.cs +++ b/Models/EducationLevel.cs @@ -6,8 +6,8 @@ namespace BMA.EHR.Recurit.Exam.Service.Models { public class EducationLevel : EntityBase { - [Required, MaxLength(100), Column(Order = 1), Comment("āļĢāļ°āļ”āļąāļšāļāļēāļĢāļĻāļķāļāļĐāļē")] - public string name { get; set; } = string.Empty; + [MaxLength(255), Column(Order = 1), Comment("āļĢāļ°āļ”āļąāļšāļāļēāļĢāļĻāļķāļāļĐāļē")] + public string? name { get; set; } = null; // [Column(Order = 2), Comment("āļŠāļ–āļēāļ™āļ°āļāļēāļĢāđƒāļŠāđ‰āļ‡āļēāļ™")] // public bool IsActive { get; set; } = true; diff --git a/Models/SubDistrict.cs b/Models/SubDistrict.cs index 71688a1..0610c37 100644 --- a/Models/SubDistrict.cs +++ b/Models/SubDistrict.cs @@ -6,11 +6,11 @@ namespace BMA.EHR.Recurit.Exam.Service.Models { public class SubDistrict : EntityBase { - [Required, MaxLength(150), Column(Order = 1), Comment("āđ€āļ‚āļ•/āļ­āļģāđ€āļ āļ­")] - public string name { get; set; } = string.Empty; + [MaxLength(255), Column(Order = 1), Comment("āđāļ‚āļ§āļ‡")] + public string? name { get; set; } = null; - [Required, MaxLength(10), Column(Order = 2), Comment("āļĢāļŦāļąāļŠāđ„āļ›āļĢāļĐāļ“āļĩāļĒāđŒ")] - public string zipCode { get; set; } = string.Empty; + [MaxLength(10), Column(Order = 2), Comment("āļĢāļŦāļąāļŠāđ„āļ›āļĢāļĐāļ“āļĩāļĒāđŒ")] + public string? zipCode { get; set; } = null; // [Column(Order = 3), Comment("āļŠāļ–āļēāļ™āļ°āļāļēāļĢāđƒāļŠāđ‰āļ‡āļēāļ™")] // public bool IsActive { get; set; } = true; diff --git a/Request/RecruitDateRequest.cs b/Request/RecruitDateRequest.cs new file mode 100644 index 0000000..b5eaee7 --- /dev/null +++ b/Request/RecruitDateRequest.cs @@ -0,0 +1,9 @@ +ïŧŋusing System.Net; + +namespace BMA.EHR.Recurit.Exam.Service.Request +{ + public class RecruitDateRequest + { + public DateTime AccountStartDate { get; set; } + } +} diff --git a/Request/RecruitPosTypeRequest.cs b/Request/RecruitPosTypeRequest.cs new file mode 100644 index 0000000..ffd4e7a --- /dev/null +++ b/Request/RecruitPosTypeRequest.cs @@ -0,0 +1,18 @@ +ïŧŋusing System.Net; + +namespace BMA.EHR.Recurit.Exam.Service.Request +{ + public class RecruitPosRequest + { + public List result { get; set; } = new(); + } + public class RecruitPosLevelRequest + { + public string posLevelName { get; set; } + public RecruitPosTypeRequest posTypes { get; set; } = new(); + } + public class RecruitPosTypeRequest + { + public string posTypeName { get; set; } + } +} diff --git a/Services/PeriodExamService.cs b/Services/PeriodExamService.cs index fc0d0d3..d206a1f 100644 --- a/Services/PeriodExamService.cs +++ b/Services/PeriodExamService.cs @@ -2999,213 +2999,291 @@ namespace BMA.EHR.Recurit.Exam.Service.Services await _contextMetadata.SaveChangesAsync(); } - public async Task UpdateAsyncDisableToPlacement(Guid examId) + public async Task UpdateAsyncDisableToPlacement(Guid examId, DateTime accountStartDate) { - var periodExam = await _context.PeriodExams.AsQueryable() - .Where(x => x.CheckDisability == true) - .FirstOrDefaultAsync(x => x.Id == examId); - - if (periodExam == null) - throw new Exception(GlobalMessages.ExamNotFound); - - var _placement = await _contextMetadata.Placements.AsQueryable() - .FirstOrDefaultAsync(x => x.PlacementType.Name == "āļ„āļąāļ”āđ€āļĨāļ·āļ­āļāļ„āļ™āļžāļīāļāļēāļĢ" && x.RefId == periodExam.Id); - if (_placement != null) - throw new Exception("āļĢāļ­āļšāļāļēāļĢāļŠāļ­āļšāļ™āļĩāđ‰āđ„āļ”āđ‰āļ—āļģāļāļēāļĢāļšāļĢāļĢāļˆāļļāđ„āļ›āđāļĨāđ‰āļ§"); - - var placement = new Placement + try { - Name = periodExam.Name, - RefId = periodExam.Id, - Round = periodExam.Round == null ? "" : periodExam.Round.ToString(), - Year = (int)(periodExam.Year == null ? 0 : periodExam.Year), - Number = await _context.Disables.AsQueryable().Where(x => x.PeriodExam == periodExam).CountAsync(), - PlacementType = await _contextMetadata.PlacementTypes.FirstOrDefaultAsync(x => x.Name.Trim().ToUpper().Contains("āļ„āļąāļ”āđ€āļĨāļ·āļ­āļāļ„āļ™āļžāļīāļāļēāļĢ")) == null ? await _contextMetadata.PlacementTypes.FirstOrDefaultAsync() : await _contextMetadata.PlacementTypes.FirstOrDefaultAsync(x => x.Name.Trim().ToUpper().Contains("āļ„āļąāļ”āđ€āļĨāļ·āļ­āļāļ„āļ™āļžāļīāļāļēāļĢ")), - StartDate = DateTime.Now, - EndDate = DateTime.Now.AddYears(2).AddDays(-1), - CreatedAt = DateTime.Now, - CreatedUserId = UserId ?? "", - CreatedFullName = FullName ?? "", - LastUpdatedAt = DateTime.Now, - LastUpdateUserId = UserId ?? "", - LastUpdateFullName = FullName ?? "", - }; - await _contextMetadata.Placements.AddAsync(placement); - var candidates = await _context.Disables.AsQueryable() - .Include(x => x.Addresses) - .Include(x => x.Certificates) - .Include(x => x.Educations) - .Include(x => x.Occupations) - .Where(x => x.PeriodExam == periodExam) - .ToListAsync(); - foreach (var candidate in candidates) - { - var IsOfficer = false; - dynamic org = null; - var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}"; - using (var client = new HttpClient()) + // 🚀 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 periodExam = await _context.PeriodExams.AsQueryable() + .Where(x => x.CheckDisability == true) + .FirstOrDefaultAsync(x => x.Id == examId); + + if (periodExam == null) + throw new Exception(GlobalMessages.ExamNotFound); + + var _placement = await _contextMetadata.Placements.AsQueryable() + .FirstOrDefaultAsync(x => x.PlacementType.Name == "āļ„āļąāļ”āđ€āļĨāļ·āļ­āļāļ„āļ™āļžāļīāļāļēāļĢ" && x.RefId == periodExam.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 { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Replace("Bearer ", "")); - client.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); - var _req = new HttpRequestMessage(HttpMethod.Get, apiUrl); - var _res = await client.SendAsync(_req); - var _result = await _res.Content.ReadAsStringAsync(); + Name = periodExam.Name, + RefId = periodExam.Id, + Round = periodExam.Round?.ToString() ?? "", + Year = (int)(periodExam.Year ?? 0), + Number = await _context.Disables.AsQueryable().Where(x => x.PeriodExam == periodExam).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); - org = JsonConvert.DeserializeObject(_result); + // 🚀 Load all related data with single queries + var candidates = await _context.Disables.AsQueryable() + .Include(x => x.Addresses) + .Include(x => x.Certificates) + .Include(x => x.Educations) + .Include(x => x.Occupations) + .Where(x => x.PeriodExam == periodExam) + .ToListAsync(); - if (org == null || org.result == null) - { - IsOfficer = false; - } - else - { - IsOfficer = true; - } - } - var Address = candidate.Addresses.FirstOrDefault() == null ? null : $"{candidate.Addresses.FirstOrDefault().Address}"; - var Moo = candidate.Addresses.FirstOrDefault() == null ? null : $" āļŦāļĄāļđāđˆ {candidate.Addresses.FirstOrDefault().Moo}"; - var Soi = candidate.Addresses.FirstOrDefault() == null ? null : $" āļ‹āļ­āļĒ {candidate.Addresses.FirstOrDefault().Soi}"; - var Road = candidate.Addresses.FirstOrDefault() == null ? null : $" āļ–āļ™āļ™ {candidate.Addresses.FirstOrDefault().Road}"; - var Address1 = candidate.Addresses.FirstOrDefault() == null ? null : $"{candidate.Addresses.FirstOrDefault().Address1}"; - var Moo1 = candidate.Addresses.FirstOrDefault() == null ? null : $" āļŦāļĄāļđāđˆ {candidate.Addresses.FirstOrDefault().Moo1}"; - var Soi1 = candidate.Addresses.FirstOrDefault() == null ? null : $" āļ‹āļ­āļĒ {candidate.Addresses.FirstOrDefault().Soi1}"; - var Road1 = candidate.Addresses.FirstOrDefault() == null ? null : $" āļ–āļ™āļ™ {candidate.Addresses.FirstOrDefault().Road1}"; var scoreImport = await _context.ScoreImports.AsQueryable() .FirstOrDefaultAsync(x => x.PeriodExam == periodExam); - var disableScore = await _context.DisableScores.AsQueryable() - .Where(x => x.ScoreImport == scoreImport) - .Where(x => x.ExamId == candidate.ExamId) - .Where(x => x.ExamStatus == "āļœāđˆāļēāļ™") - .FirstOrDefaultAsync(x => x.ExamId == candidate.ExamId && x.ScoreImport == scoreImport); - if (disableScore == null) - continue; - var placementProfile = new PlacementProfile + + var disableScores = await _context.DisableScores.AsQueryable() + .Where(x => x.ScoreImport == scoreImport && x.ExamStatus == "āļœāđˆāļēāļ™") + .ToListAsync(); + + var disableScoresDict = disableScores + .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 => { - Placement = placement, - PositionCandidate = candidate.PositionName, - PositionType = candidate.PositionType, - PositionLevel = candidate.PositionLevel, - 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 = _contextOrg.province.FirstOrDefault(x => x.name == candidate.CitizenCardIssuer)?.Id ?? null, - CitizenDate = candidate.CitizenCardExpireDate, - Telephone = candidate?.Addresses?.FirstOrDefault()?.Telephone ?? null, - MobilePhone = candidate?.Addresses?.FirstOrDefault()?.Mobile ?? null, - RegistAddress = $"{Address}{Moo}{Soi}{Road}", - RegistProvinceId = _contextOrg.province.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Province ?? ""))?.Id ?? null, - RegistDistrictId = _contextOrg.district.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.District ?? ""))?.Id ?? null, - RegistSubDistrictId = _contextOrg.subDistrict.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Soi ?? ""))?.Id ?? null, - RegistZipCode = candidate?.Addresses?.FirstOrDefault()?.ZipCode ?? null, - RegistSame = false, - CurrentAddress = $"{Address1}{Moo1}{Soi1}{Road1}", - CurrentProvinceId = _contextOrg.province.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Province1 ?? ""))?.Id ?? null, - CurrentDistrictId = _contextOrg.district.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.District1 ?? ""))?.Id ?? null, - CurrentSubDistrictId = _contextOrg.subDistrict.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Soi1 ?? ""))?.Id ?? null, - CurrentZipCode = candidate?.Addresses?.FirstOrDefault()?.ZipCode1 ?? null, - Marry = candidate?.Marry?.Contains("āļŠāļĄāļĢāļŠ") ?? false, + if (string.IsNullOrWhiteSpace(candidate.CitizenId)) + return new { CitizenId = candidate.CitizenId ?? "", org = (dynamic?)null }; - OccupationPositionType = "other", - OccupationTelephone = candidate?.Occupations?.FirstOrDefault()?.Telephone ?? null, - OccupationPosition = candidate?.Occupations?.FirstOrDefault()?.Position ?? 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(); - PointTotalA = disableScore == null ? null : Convert.ToDouble(disableScore.FullA), - PointA = disableScore == null ? null : Convert.ToDouble(disableScore.SumA), - PointTotalB = disableScore == null ? null : Convert.ToDouble(disableScore.FullB), - PointB = disableScore == null ? null : Convert.ToDouble(disableScore.SumB), - PointTotalC = disableScore == null ? null : Convert.ToDouble(disableScore.FullC), - PointC = disableScore == null ? null : Convert.ToDouble(disableScore.SumC), - ExamNumber = disableScore == null || int.TryParse(disableScore.Number, out int n) == false ? null : Convert.ToInt32(disableScore.Number), - ExamRound = null, - IsRelief = false, - PlacementStatus = "UN-CONTAIN", - Pass = disableScore == null ? null : disableScore.ExamStatus, - RemarkHorizontal = "āđ‚āļ”āļĒāļĄāļĩāđ€āļ‡āļ·āđˆāļ­āļ™āđ„āļ‚āļ§āđˆāļēāļ•āđ‰āļ­āļ‡āļ›āļāļīāļšāļąāļ•āļīāļ‡āļēāļ™āđƒāļŦāđ‰āļāļĢāļļāļ‡āđ€āļ—āļžāļĄāļŦāļēāļ™āļ„āļĢāđ€āļ›āđ‡āļ™āļĢāļ°āļĒāļ°āđ€āļ§āļĨāļēāđ„āļĄāđˆāļ™āđ‰āļ­āļĒāļāļ§āđˆāļē āđ• āļ›āļĩ āļ™āļąāļšāđāļ•āđˆāļ§āļąāļ™āļ—āļĩāđˆāđ„āļ”āđ‰āļĢāļąāļšāļāļēāļĢāļšāļĢāļĢāļˆāļļāđāļĨāļ°āđāļ•āđˆāļ‡āļ•āļąāđ‰āļ‡ āđ‚āļ”āļĒāļŦāđ‰āļēāļĄāđ‚āļ­āļ™āđ„āļ›āļŦāļ™āđˆāļ§āļĒāļ‡āļēāļ™āļŦāļĢāļ·āļ­āļŠāđˆāļ§āļ™āļĢāļēāļŠāļāļēāļĢāļ­āļ·āđˆāļ™ āđ€āļ§āđ‰āļ™āđ€āđ€āļ•āđˆāļĨāļēāļ­āļ­āļāļˆāļēāļāļĢāļēāļŠāļāļēāļĢ", - Amount = org?.result?.amount ?? null, - PositionSalaryAmount = org?.result?.positionSalaryAmount ?? null, - MouthSalaryAmount = org?.result?.mouthSalaryAmount ?? null, - CreatedAt = DateTime.Now, - CreatedUserId = UserId ?? "", - CreatedFullName = FullName ?? "", - LastUpdatedAt = DateTime.Now, - LastUpdateUserId = UserId ?? "", - LastUpdateFullName = FullName ?? "", - IsOfficer = IsOfficer, - profileId = org?.result?.profileId ?? null, - IsOld = org == null || org.result == null ? false : true, - AmountOld = org?.result?.AmountOld ?? null, - nodeOld = org?.result?.node ?? null, - nodeIdOld = org?.result?.nodeId ?? null, - posmasterIdOld = org?.result?.posmasterId ?? null, - rootOld = org?.result?.root ?? null, - rootIdOld = org?.result?.rootId ?? null, - rootShortNameOld = org?.result?.rootShortName ?? null, - child1Old = org?.result?.child1 ?? null, - child1IdOld = org?.result?.child1Id ?? null, - child1ShortNameOld = org?.result?.child1ShortName ?? null, - child2Old = org?.result?.child2 ?? null, - child2IdOld = org?.result?.child2Id ?? null, - child2ShortNameOld = org?.result?.child2ShortName ?? null, - child3Old = org?.result?.child3 ?? null, - child3IdOld = org?.result?.child3Id ?? null, - child3ShortNameOld = org?.result?.child3ShortName ?? null, - child4Old = org?.result?.child4 ?? null, - child4IdOld = org?.result?.child4Id ?? null, - child4ShortNameOld = org?.result?.child4ShortName ?? null, - orgRevisionIdOld = org?.result?.orgRevisionId ?? null, - posMasterNoOld = org?.result?.posMasterNo ?? null, - positionNameOld = org?.result?.position ?? null, - posTypeIdOld = org?.result?.posTypeId ?? null, - posTypeNameOld = org?.result?.posTypeName ?? null, - posLevelIdOld = org?.result?.posLevelId ?? null, - posLevelNameOld = org?.result?.posLevelName ?? null, - }; - await _contextMetadata.PlacementProfiles.AddAsync(placementProfile); + var orgResults = await Task.WhenAll(orgTasks); + var orgDict = orgResults.ToDictionary(x => x.CitizenId ?? "", x => x.org); - var placementEducation = new PlacementEducation + // 🚀 Prepare batch inserts + var placementProfiles = new List(); + var placementEducations = new List(); + var placementCertificates = new List(); + + foreach (var candidate in candidates) { - PlacementProfile = placementProfile, - EducationLevelId = _contextOrg.educationLevel.FirstOrDefault(x => x.name == (candidate!.Educations!.FirstOrDefault()!.HighDegree ?? ""))?.Id ?? null, - EducationLevelName = _contextOrg.educationLevel.FirstOrDefault(x => x.name == (candidate!.Educations!.FirstOrDefault()!.HighDegree ?? ""))?.name ?? null, - Field = candidate?.Educations?.FirstOrDefault()?.Major ?? null, - Gpa = candidate?.Educations?.FirstOrDefault()?.GPA!.ToString() ?? null, - Institute = candidate?.Educations?.FirstOrDefault()?.University ?? null, - Degree = candidate?.Educations?.FirstOrDefault()?.Degree ?? null, - FinishDate = candidate?.Educations?.FirstOrDefault()?.BachelorDate ?? null, - IsDate = true, - CreatedAt = DateTime.Now, - CreatedUserId = UserId ?? "", - LastUpdatedAt = DateTime.Now, - LastUpdateUserId = UserId ?? "", - CreatedFullName = FullName ?? "", - LastUpdateFullName = FullName ?? "", - }; - await _contextMetadata.PlacementEducations.AddAsync(placementEducation); + if (string.IsNullOrWhiteSpace(candidate.ExamId) || + !disableScoresDict.TryGetValue(candidate.ExamId, out var disableScore)) + continue; - var placementCertificate = new PlacementCertificate - { - PlacementProfile = placementProfile, - CertificateNo = candidate?.Certificates?.FirstOrDefault()?.CertificateNo ?? null, - IssueDate = candidate?.Certificates?.FirstOrDefault()?.IssueDate ?? null, - ExpireDate = candidate?.Certificates?.FirstOrDefault()?.ExpiredDate ?? null, - CertificateType = candidate?.Certificates?.FirstOrDefault()?.Description ?? null, - CreatedAt = DateTime.Now, - CreatedUserId = UserId ?? "", - LastUpdatedAt = DateTime.Now, - LastUpdateUserId = UserId ?? "", - CreatedFullName = FullName ?? "", - LastUpdateFullName = FullName ?? "", - }; - await _contextMetadata.PlacementCertificates.AddAsync(placementCertificate); + 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 = string.Join("", new[] { + firstAddress?.Address ?? "", + string.IsNullOrWhiteSpace(firstAddress?.Moo) ? "" : $" āļŦāļĄāļđāđˆ {firstAddress.Moo}", + string.IsNullOrWhiteSpace(firstAddress?.Soi) ? "" : $" āļ‹āļ­āļĒ {firstAddress.Soi}", + string.IsNullOrWhiteSpace(firstAddress?.Road) ? "" : $" āļ–āļ™āļ™ {firstAddress.Road}" + }); + + var currentAddress = string.Join("", new[] { + firstAddress?.Address1 ?? "", + string.IsNullOrWhiteSpace(firstAddress?.Moo1) ? "" : $" āļŦāļĄāļđāđˆ {firstAddress.Moo1}", + string.IsNullOrWhiteSpace(firstAddress?.Soi1) ? "" : $" āļ‹āļ­āļĒ {firstAddress.Soi1}", + string.IsNullOrWhiteSpace(firstAddress?.Road1) ? "" : $" āļ–āļ™āļ™ {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?.District)?.Id, + RegistSubDistrictId = subDistrictsCache.FirstOrDefault(x => x.name == firstAddress?.Soi)?.Id, + RegistZipCode = firstAddress?.ZipCode ?? "", + RegistSame = false, + CurrentAddress = currentAddress, + CurrentProvinceId = provincesCache.FirstOrDefault(x => x.name == firstAddress?.Province1)?.Id, + CurrentDistrictId = districtsCache.FirstOrDefault(x => x.name == firstAddress?.District1)?.Id, + CurrentSubDistrictId = subDistrictsCache.FirstOrDefault(x => x.name == firstAddress?.Soi1)?.Id, + CurrentZipCode = firstAddress?.ZipCode1, + Marry = candidate.Marry?.Contains("āļŠāļĄāļĢāļŠ") ?? false, + OccupationPositionType = "other", + OccupationTelephone = firstOccupation?.Telephone ?? "", + OccupationPosition = firstOccupation?.Position ?? "", + PointTotalA = Convert.ToDouble(disableScore.FullA), + PointA = Convert.ToDouble(disableScore.SumA), + PointTotalB = Convert.ToDouble(disableScore.FullB), + PointB = Convert.ToDouble(disableScore.SumB), + PointTotalC = Convert.ToDouble(disableScore.FullC), + PointC = Convert.ToDouble(disableScore.SumC), + ExamNumber = !string.IsNullOrWhiteSpace(disableScore.Number) && int.TryParse(disableScore.Number, out int n) ? n : null, + ExamRound = null, + IsRelief = false, + PlacementStatus = "UN-CONTAIN", + Pass = disableScore.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?.AmountOld, + 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?.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; } - await _contextMetadata.SaveChangesAsync(); }