diff --git a/sql_seed/update_profile_position_fields.sql b/sql_seed/update_profile_position_fields.sql new file mode 100644 index 00000000..e9e999dc --- /dev/null +++ b/sql_seed/update_profile_position_fields.sql @@ -0,0 +1,154 @@ +-- ===================================================== +-- Update position fields in profile table +-- อัพเดทฟิลด์ตำแหน่งในตาราง profile +-- +-- Fields: +-- - positionField (สายงาน) +-- - posExecutive (ตำแหน่งทางการบริหาร) +-- - positionArea (ด้าน/สาขา) +-- - positionExecutiveField (ด้านทางการบริหาร) +-- - posMasterNo (เลขที่ตำแหน่ง) - format: orgShortName + space + number +-- - org (สังกัด) +-- +-- Run each query separately to verify results +-- ===================================================== +USE hrms_organization; +-- 1. Update positionField (สายงาน) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +SET p.positionField = pos.positionField +WHERE p.positionField IS NULL; + +-- 2. Update posExecutive (ตำแหน่งทางการบริหาร) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +INNER JOIN posExecutive pe ON pos.posExecutiveId = pe.id +SET p.posExecutive = pe.posExecutiveName +WHERE p.posExecutive IS NULL; + +-- 3. Update positionArea (ด้าน/สาขา) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +SET p.positionArea = pos.positionArea +WHERE p.positionArea IS NULL; + +-- 4. Update positionExecutiveField (ด้านทางการบริหาร) +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +SET p.positionExecutiveField = pos.positionExecutiveField +WHERE p.positionExecutiveField IS NULL; + +-- 5. Update posMasterNo (เลขที่ตำแหน่ง) - format: orgShortName + space + number +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +LEFT JOIN orgRoot r ON pm.orgRootId = r.id +LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id +LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id +LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id +LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id +SET p.posMasterNo = TRIM(CONCAT( + CASE + WHEN pm.orgChild1Id IS NULL THEN r.orgRootShortName + WHEN pm.orgChild2Id IS NULL THEN c1.orgChild1ShortName + WHEN pm.orgChild3Id IS NULL THEN c2.orgChild2ShortName + WHEN pm.orgChild4Id IS NULL THEN c3.orgChild3ShortName + ELSE c4.orgChild4ShortName + END, + ' ', + CONCAT_WS('', pm.posMasterNoPrefix, pm.posMasterNo, pm.posMasterNoSuffix) +)) +WHERE p.posMasterNo IS NULL; + +-- 6. Update org (สังกัด) - combine all org levels +UPDATE profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +LEFT JOIN orgRoot r ON pm.orgRootId = r.id +LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id +LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id +LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id +LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id +SET p.org = TRIM(CONCAT_WS( + CHAR(10), + c4.orgChild4Name, + c3.orgChild3Name, + c2.orgChild2Name, + c1.orgChild1Name, + r.orgRootName +)) +WHERE p.org IS NULL; + +-- ===================================================== +-- เช็คผลลัพธ์ (Check results) +-- ===================================================== + +-- เช็คจำนวนที่ update ได้ +SELECT + COUNT(CASE WHEN positionField IS NOT NULL THEN 1 END) AS has_positionField, + COUNT(CASE WHEN posExecutive IS NOT NULL THEN 1 END) AS has_posExecutive, + COUNT(CASE WHEN positionArea IS NOT NULL THEN 1 END) AS has_positionArea, + COUNT(CASE WHEN positionExecutiveField IS NOT NULL THEN 1 END) AS has_positionExecutiveField, + COUNT(CASE WHEN posMasterNo IS NOT NULL THEN 1 END) AS has_posMasterNo, + COUNT(CASE WHEN org IS NOT NULL THEN 1 END) AS has_org +FROM profile; + +-- ===================================================== +-- SELECT query สำหรับทดสอบก่อนรัน (Test before run) +-- ===================================================== + +SELECT + p.id, + p.firstName, + p.lastName, + p.citizenId, + + p.positionField as old_positionField, + p.posExecutive as old_posExecutive, + p.positionArea as old_positionArea, + p.positionExecutiveField as old_positionExecutiveField, + p.posMasterNo as old_posMasterNo, + p.org as old_org, + + pos.positionField as new_positionField, + pe.posExecutiveName as new_posExecutive, + pos.positionArea as new_positionArea, + pos.positionExecutiveField as new_positionExecutiveField, + + TRIM(CONCAT( + CASE + WHEN pm.orgChild1Id IS NULL THEN r.orgRootShortName + WHEN pm.orgChild2Id IS NULL THEN c1.orgChild1ShortName + WHEN pm.orgChild3Id IS NULL THEN c2.orgChild2ShortName + WHEN pm.orgChild4Id IS NULL THEN c3.orgChild3ShortName + ELSE c4.orgChild4ShortName + END, + ' ', + pm.posMasterNo + )) as new_posMasterNo, + + TRIM(CONCAT_WS(CHAR(10), c4.orgChild4Name, c3.orgChild3Name, c2.orgChild2Name, c1.orgChild1Name, r.orgRootName)) as new_org + +FROM profile p +INNER JOIN posMaster pm ON pm.current_holderId = p.id +INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0 +INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1 +LEFT JOIN posExecutive pe ON pos.posExecutiveId = pe.id +LEFT JOIN orgRoot r ON pm.orgRootId = r.id +LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id +LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id +LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id +LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id + +-- ใส่ WHERE ทดสอบ 1 คน (Test 1 person) +WHERE p.id = 'ใส่ profile_id ที่ต้องการทดสอบ' +-- หรือทดสอบ 10 คน (Test 10 persons) +-- LIMIT 10; diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index da0ed3fe..ba498c1e 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -48,6 +48,7 @@ import { import { Position } from "../entities/Position"; import { PosMaster } from "../entities/PosMaster"; import { EmployeePosition } from "../entities/EmployeePosition"; +import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { ProfileDiscipline } from "../entities/ProfileDiscipline"; import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory"; @@ -3660,6 +3661,7 @@ export class CommandController extends Controller { const posMaster = await this.posMasterRepository.findOne({ where: { id: item.posmasterId }, + relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], }); if (posMaster == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); @@ -3715,14 +3717,22 @@ export class CommandController extends Controller { id: item.positionId, posMasterId: item.posmasterId, }, + relations: ["posExecutive"], }); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); if(!posMaster.isSit){ profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; profile.position = positionNew.positionName; + profile.positionField = positionNew.positionField ?? null; + profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; + profile.positionArea = positionNew.positionArea ?? null; + profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; } profile.amount = item.amount ?? null; profile.amountSpecial = item.amountSpecial ?? null; @@ -6500,6 +6510,7 @@ export class CommandController extends Controller { relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], }); let _oldInsigniaIds: string[] = []; + let _oldSalaries: any[] = []; //ลูกจ้างประจำ หรือ บุคคลภายนอก if (!profile) { //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม @@ -6608,6 +6619,11 @@ export class CommandController extends Controller { profile.isLeave && ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) ) { + //ดึง profileSalary เดิม + _oldSalaries = await this.salaryRepo.find({ + where: { profileId: profile.id }, + order: { order: "ASC" }, + }); if (profile.profileInsignias.length > 0) { _oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? []; } @@ -6846,6 +6862,23 @@ export class CommandController extends Controller { await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); } //Salary + //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ + if (_oldSalaries.length > 0) { + await Promise.all( + _oldSalaries.map(async (oldSal) => { + const profileSal: any = new ProfileSalary(); + Object.assign(profileSal, { ...oldSal, ...meta }); + const salaryHistory = new ProfileSalaryHistory(); + Object.assign(salaryHistory, { ...profileSal, id: undefined }); + profileSal.profileId = profile.id; + await this.salaryRepo.save(profileSal, { data: req }); + setLogDataDiff(req, { before, after: profileSal }); + salaryHistory.profileSalaryId = profileSal.id; + await this.salaryHistoryRepo.save(salaryHistory, { data: req }); + }), + ); + } + //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว if (item.bodySalarys && item.bodySalarys != null) { const dest_item = await this.salaryRepo.findOne({ where: { profileId: profile.id }, @@ -6876,7 +6909,7 @@ export class CommandController extends Controller { where: { id: item.bodyPosition.posmasterId, }, - relations: { orgRevision: true } + relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true } }); // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ @@ -6893,9 +6926,8 @@ export class CommandController extends Controller { orgRevisionIsDraft: false } }, - relations: { orgRevision: true } - }); - } + relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true } + }); } if (posMaster == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); @@ -6983,20 +7015,28 @@ export class CommandController extends Controller { id: item.bodyPosition.positionId, posMasterId: posMaster.id, }, + relations: ["posExecutive"], }); } // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); if(!posMaster.isSit){ profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; profile.position = positionNew.positionName; + profile.positionField = positionNew.positionField ?? null; + profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; + profile.positionArea = positionNew.positionArea ?? null; + profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; // profile.dateStart = new Date(); - await this.profileRepository.save(profile, { data: req }); - setLogDataDiff(req, { before, after: profile }); } + await this.profileRepository.save(profile, { data: req }); + setLogDataDiff(req, { before, after: profile }); await this.positionRepository.save(positionNew, { data: req }); } // await CreatePosMasterHistoryOfficer(posMaster.id, req); diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index 128cb4d1..c8ffe5da 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -15,6 +15,7 @@ import { import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { addLogSequence } from "../interfaces/utils"; +import HttpSuccess from "../interfaces/http-success"; interface CachedToken { token: string; @@ -88,7 +89,8 @@ export class ExRetirementController extends Controller { }, }); - return res.data; + // return res.data; + return new HttpSuccess(res.data.data); } catch (error: any) { if (error.response?.status === 500 && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); diff --git a/src/controllers/ImportDataController.ts b/src/controllers/ImportDataController.ts index 5b8ca808..b424edbb 100644 --- a/src/controllers/ImportDataController.ts +++ b/src/controllers/ImportDataController.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Route, Security, Tags, Request, UploadedFile } from "tsoa"; +import { Controller, Post, Route, Security, Tags, Request, UploadedFile, Path } from "tsoa"; import { AppDataSource } from "../database/data-source"; import { In, IsNull, LessThanOrEqual, Not, Between } from "typeorm"; import HttpSuccess from "../interfaces/http-success"; @@ -105,6 +105,7 @@ import { positionOfficer } from "../entities/mis/positionOfficer"; import { ProvinceMaster } from "../entities/ProvinceMaster"; import { SubDistrictMaster } from "../entities/SubDistrictMaster"; import { DistrictMaster } from "../entities/DistrictMaster"; +import { RequestWithUser } from "../middlewares/user"; @Route("api/v1/org/upload") @Tags("UPLOAD") @Security("bearerAuth") @@ -6815,4 +6816,523 @@ export class ImportDataController extends Controller { // await repo.save(entities); // return entities; // } + + /** + * @summary Import ข้อมูลประวัติตำแหน่งเงินเดือนของข้าราชการเข้าตาราง ProfileSalaryTemp + * @param profileId Id โปรไฟล์ข้าราชการ + * @param file Excel file with salary history data + */ + @Post("office-profileSalaryTemp/{profileId}") + @UseInterceptors(FileInterceptor("file")) + async UploadProfileSalaryTemp( + @Path() profileId: string, + @Request() req: RequestWithUser, + @UploadedFile() file: Express.Multer.File, + ) { + if (!profileId) { + throw new Error("profileId is required"); + } + + // อ่านไฟล์ Excel ก่อน (นอก transaction) + const workbook = xlsx.read(file.buffer, { type: "buffer" }); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][]; + + let salaryTemps: ProfileSalaryTemp[] = []; + let dateTime = new Date(); + + // เริ่มจาก index 1 เพื่อข้าม header row + for (let i = 1; i < getExcel.length; i++) { + const row = getExcel[i]; + + // ข้าม empty rows + if (!row || row.length === 0) { + continue; + } + + // ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง) + if (!row[0]) { + continue; + } + + const salaryTemp = new ProfileSalaryTemp(); + + // ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number + const parseExcelDate = (value: any): Date | null => { + if (!value) return null; + + // กรณี 1: Excel serial number (ตัวเลข) + if (typeof value === "number") { + // Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900 + // แปลงเป็น JavaScript Date (epoch 1970) + let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds(), + ); + } + return jsDate; + } + + // กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy) + const dateStr = value.toString().trim(); + + // ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่ + if (/^\d+$/.test(dateStr)) { + const serialNum = parseInt(dateStr); + let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds(), + ); + } + return jsDate; + } + + // String format ปกติ (dd/mm/yyyy) + const dateParts = dateStr.split("/"); + if (dateParts.length === 3) { + // แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก + const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0"); + const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0"); + let year = parseInt(dateParts[2].trim()); + if (year > 2500) { + year -= 543; + } + const result = new Date(`${year}-${month}-${day}`); + return result; + } + + return null; + }; + + // Index 1: วันที่คำสั่งมีผล + let commandDateAffect: Date | null = null; + if (row[1]) { + commandDateAffect = parseExcelDate(row[1]); + } + + // Index 25: วันที่ลงนาม + let commandDateSign: Date | null = null; + if (row[25]) { + commandDateSign = parseExcelDate(row[25]); + } + + // Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column + // ข้อมูลระบบ + salaryTemp.profileId = profileId; + salaryTemp.profileEmployeeId = null as any; + + // Index 0: ลำดับ + salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any); + + // Index 1: วันที่คำสั่งมีผล + salaryTemp.commandDateAffect = commandDateAffect as any; + + // Index 2: ตำแหน่งในสายงาน + salaryTemp.positionName = row[2] || null; + + // Index 3: ตำแหน่งประเภท + salaryTemp.positionType = row[3] || null; + + // Index 4: ระดับ + salaryTemp.positionLevel = row[4] || null; + + // Index 5: ระดับซี + salaryTemp.positionCee = row[5] || null; + + // Index 6: สายงาน + salaryTemp.positionLine = row[6] || null; + + // Index 7: ด้าน/สาขา + salaryTemp.positionPathSide = row[7] || null; + + // Index 8: ตำแหน่งทางการบริหาร + salaryTemp.positionExecutive = row[8] || null; + + // Index 9: ด้านทางการบริหาร + salaryTemp.positionExecutiveField = row[9] || null; + + // Index 10: เงินเดือน + salaryTemp.amount = row[10] || 0; + + // Index 11: เงินค่าตอบแทนรายเดือน + salaryTemp.mouthSalaryAmount = row[11] || 0; + + // Index 12: เงินประจำตำแหน่ง + salaryTemp.positionSalaryAmount = row[12] || 0; + + // Index 13: เงินค่าตอบแทนพิเศษ + salaryTemp.amountSpecial = row[13] || 0; + + // Index 14: หน่วยงาน + salaryTemp.orgRoot = row[14] || null; + + // Index 15: ส่วนราชการระดับ 1 + salaryTemp.orgChild1 = row[15] || null; + + // Index 16: ส่วนราชการระดับ 2 + salaryTemp.orgChild2 = row[16] || null; + + // Index 17: ส่วนราชการระดับ 3 + salaryTemp.orgChild3 = row[17] || null; + + // Index 18: ส่วนราชการระดับ 4 + salaryTemp.orgChild4 = row[18] || null; + + // Index 19: ตัวย่อเลขที่ตำแหน่ง + salaryTemp.posNoAbb = row[19] || null; + + // Index 20: เลขที่ตำแหน่ง + salaryTemp.posNo = row[20] ? row[20].toString() : null; + + // Index 21: หน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSit = row[21] || null; + + // Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSitAbb = row[22] || null; + + // Index 23: เลขที่คำสั่ง + salaryTemp.commandNo = row[23] || null; + + // Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.) + let commandYearValue: number | null = null; + if (row[24]) { + commandYearValue = parseInt(row[24].toString()); + // ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ. + if (commandYearValue > 2500) { + commandYearValue -= 543; + } + } + salaryTemp.commandYear = commandYearValue as any; + + // Index 25: วันที่ลงนาม (แปลงแล้ว) + salaryTemp.commandDateSign = commandDateSign as any; + + // Index 26: ประเภทคำสั่ง + salaryTemp.commandName = row[26] || null; + + // Index 27: หมายเหตุ + salaryTemp.remark = row[27] || null; + + // Index 28: commandId + salaryTemp.commandId = row[28] || null; + + // Index 29: commandCode + salaryTemp.commandCode = row[29] || null; + + // ข้อมูลระบบ + salaryTemp.isDelete = false; + salaryTemp.isEdit = false; + salaryTemp.isGovernment = false; + salaryTemp.isEntry = false; + salaryTemp.createdAt = dateTime; + salaryTemp.createdUserId = req.user?.sub || ""; + salaryTemp.createdFullName = req.user?.name || "System Administrator"; + salaryTemp.lastUpdatedAt = dateTime; + salaryTemp.lastUpdateUserId = req.user?.sub || ""; + salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator"; + + // 12,15,16 isGovernment = false & dateGovernment = salaryTemp.commandDateAffect + if (["12", "15", "16"].includes(salaryTemp.commandCode ?? "")) { + salaryTemp.isGovernment = false; + salaryTemp.dateGovernment = salaryTemp.commandDateAffect; + } + // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = salaryTemp.commandDateAffect + else if (["1", "2", "3", "4", "10", "11", "20"].includes(salaryTemp.commandCode ?? "")) { + salaryTemp.isGovernment = true; + salaryTemp.dateGovernment = salaryTemp.commandDateAffect; + } + + salaryTemps.push(salaryTemp); + } + + // ใช้ Transaction เพื่อความปลอดภัย + await AppDataSource.transaction(async (transactionalEntityManager) => { + // ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileId นั้น + await transactionalEntityManager.delete(ProfileSalaryTemp, { profileId }); + // Insert ข้อมูลใหม่ + await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps); + }); + + return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length }); + } + + /** + * @summary Import ข้อมูลประวัติตำแหน่งเงินเดือนของลูกจ้างประจำเข้าตาราง ProfileSalaryTemp + * @param profileEmployeeId Id โปรไฟล์ลูกจ้างประจำ + * @param file Excel file with salary history data + */ + @Post("employee-profileSalaryTemp/{profileEmployeeId}") + @UseInterceptors(FileInterceptor("file")) + async UploadProfileEmployeeSalaryTemp( + @Path() profileEmployeeId: string, + @Request() req: RequestWithUser, + @UploadedFile() file: Express.Multer.File, + ) { + if (!profileEmployeeId) { + throw new Error("profileEmployeeId is required"); + } + + // อ่านไฟล์ Excel ก่อน (นอก transaction) + const workbook = xlsx.read(file.buffer, { type: "buffer" }); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][]; + + let salaryTemps: ProfileSalaryTemp[] = []; + let dateTime = new Date(); + + // เริ่มจาก index 1 เพื่อข้าม header row + for (let i = 1; i < getExcel.length; i++) { + const row = getExcel[i]; + + // ข้าม empty rows + if (!row || row.length === 0) { + continue; + } + + // ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง) + if (!row[0]) { + continue; + } + + const salaryTemp = new ProfileSalaryTemp(); + + // ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number + const parseExcelDate = (value: any): Date | null => { + if (!value) return null; + + // กรณี 1: Excel serial number (ตัวเลข) + if (typeof value === "number") { + // Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900 + // แปลงเป็น JavaScript Date (epoch 1970) + let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds(), + ); + } + return jsDate; + } + + // กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy) + const dateStr = value.toString().trim(); + + // ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่ + if (/^\d+$/.test(dateStr)) { + const serialNum = parseInt(dateStr); + let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000)); + + // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) + if (jsDate.getFullYear() > 2500) { + const newYear = jsDate.getFullYear() - 543; + jsDate = new Date( + newYear, + jsDate.getMonth(), + jsDate.getDate(), + jsDate.getHours(), + jsDate.getMinutes(), + jsDate.getSeconds(), + jsDate.getMilliseconds(), + ); + } + return jsDate; + } + + // String format ปกติ (dd/mm/yyyy) + const dateParts = dateStr.split("/"); + if (dateParts.length === 3) { + // แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก + const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0"); + const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0"); + let year = parseInt(dateParts[2].trim()); + if (year > 2500) { + year -= 543; + } + const result = new Date(`${year}-${month}-${day}`); + return result; + } + + return null; + }; + + // Index 1: วันที่คำสั่งมีผล + let commandDateAffect: Date | null = null; + if (row[1]) { + commandDateAffect = parseExcelDate(row[1]); + } + + // Index 25: วันที่ลงนาม + let commandDateSign: Date | null = null; + if (row[25]) { + commandDateSign = parseExcelDate(row[25]); + } + + // Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column + // ข้อมูลระบบ + salaryTemp.profileEmployeeId = profileEmployeeId; + salaryTemp.profileId = null as any; + + // Index 0: ลำดับ + salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any); + + // Index 1: วันที่คำสั่งมีผล + salaryTemp.commandDateAffect = commandDateAffect as any; + + // Index 2: ตำแหน่งในสายงาน + salaryTemp.positionName = row[2] || null; + + // Index 3: ตำแหน่งประเภท + salaryTemp.positionType = row[3] || null; + + // Index 4: ระดับ + salaryTemp.positionLevel = row[4] || null; + + // Index 5: ระดับซี + salaryTemp.positionCee = row[5] || null; + + // Index 6: สายงาน + salaryTemp.positionLine = row[6] || null; + + // Index 7: ด้าน/สาขา + salaryTemp.positionPathSide = row[7] || null; + + // Index 8: ตำแหน่งทางการบริหาร + salaryTemp.positionExecutive = row[8] || null; + + // Index 9: ด้านทางการบริหาร + salaryTemp.positionExecutiveField = row[9] || null; + + // Index 10: เงินเดือน + salaryTemp.amount = row[10] || 0; + + // Index 11: เงินค่าตอบแทนรายเดือน + salaryTemp.mouthSalaryAmount = row[11] || 0; + + // Index 12: เงินประจำตำแหน่ง + salaryTemp.positionSalaryAmount = row[12] || 0; + + // Index 13: เงินค่าตอบแทนพิเศษ + salaryTemp.amountSpecial = row[13] || 0; + + // Index 14: หน่วยงาน + salaryTemp.orgRoot = row[14] || null; + + // Index 15: ส่วนราชการระดับ 1 + salaryTemp.orgChild1 = row[15] || null; + + // Index 16: ส่วนราชการระดับ 2 + salaryTemp.orgChild2 = row[16] || null; + + // Index 17: ส่วนราชการระดับ 3 + salaryTemp.orgChild3 = row[17] || null; + + // Index 18: ส่วนราชการระดับ 4 + salaryTemp.orgChild4 = row[18] || null; + + // Index 19: ตัวย่อเลขที่ตำแหน่ง + salaryTemp.posNoAbb = row[19] || null; + + // Index 20: เลขที่ตำแหน่ง + salaryTemp.posNo = row[20] ? row[20].toString() : null; + + // Index 21: หน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSit = row[21] || null; + + // Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง + salaryTemp.posNumCodeSitAbb = row[22] || null; + + // Index 23: เลขที่คำสั่ง + salaryTemp.commandNo = row[23] || null; + + // Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.) + let commandYearValue: number | null = null; + if (row[24]) { + commandYearValue = parseInt(row[24].toString()); + // ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ. + if (commandYearValue > 2500) { + commandYearValue -= 543; + } + } + salaryTemp.commandYear = commandYearValue as any; + + // Index 25: วันที่ลงนาม (แปลงแล้ว) + salaryTemp.commandDateSign = commandDateSign as any; + + // Index 26: ประเภทคำสั่ง + salaryTemp.commandName = row[26] || null; + + // Index 27: หมายเหตุ + salaryTemp.remark = row[27] || null; + + // Index 28: commandId + salaryTemp.commandId = row[28] || null; + + // Index 29: commandCode + salaryTemp.commandCode = row[29] || null; + + // ข้อมูลระบบ + salaryTemp.isDelete = false; + salaryTemp.isEdit = false; + salaryTemp.isGovernment = false; + salaryTemp.isEntry = false; + salaryTemp.createdAt = dateTime; + salaryTemp.createdUserId = req.user?.sub || ""; + salaryTemp.createdFullName = req.user?.name || "System Administrator"; + salaryTemp.lastUpdatedAt = dateTime; + salaryTemp.lastUpdateUserId = req.user?.sub || ""; + salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator"; + + // 12,15,16 isGovernment = false & dateGovernment = salaryTemp.commandDateAffect + if (["12", "15", "16"].includes(salaryTemp.commandCode ?? "")) { + salaryTemp.isGovernment = false; + salaryTemp.dateGovernment = salaryTemp.commandDateAffect; + } + // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = salaryTemp.commandDateAffect + else if (["1", "2", "3", "4", "10", "11", "20"].includes(salaryTemp.commandCode ?? "")) { + salaryTemp.isGovernment = true; + salaryTemp.dateGovernment = salaryTemp.commandDateAffect; + } + salaryTemps.push(salaryTemp); + } + + // ใช้ Transaction เพื่อความปลอดภัย + await AppDataSource.transaction(async (transactionalEntityManager) => { + // ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileEmployeeId นั้น + await transactionalEntityManager.delete(ProfileSalaryTemp, { profileEmployeeId }); + // Insert ข้อมูลใหม่ + await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps); + }); + + return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length }); + } } diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts index 995fa3c0..0a749732 100644 --- a/src/controllers/KeycloakSyncController.ts +++ b/src/controllers/KeycloakSyncController.ts @@ -315,4 +315,81 @@ export class KeycloakSyncController extends Controller { ...result, }); } + + /** + * Sync profiles with missing empType for a specific month (Admin only) + * + * @summary Find profiles updated in specified month with missing empType in Keycloak and sync them (ADMIN) + * + * @description + * This endpoint will: + * - List profiles from Profile table where lastUpdatedAt falls within the specified month + * - For each profile, check Keycloak if empType attribute is empty/null + * - If empType is empty, sync the profile using existing sync logic + * - Return summary of sync results + * + * Features: + * - Dry run mode (dryRun=true) to check without syncing + * - Configurable concurrency for parallel processing + * - Rate limiting to avoid overwhelming Keycloak + * - Detailed error reporting + * - Idempotent (can be safely re-run) + * + * @param {request} request Request body containing month parameter + * @param dryRun - If true, only check without syncing (default: false) + * @param concurrency - Number of parallel operations (default: 5) + * @param rateLimit - Requests per second limit (default: 10) + */ + @Post("sync-missing-emptype") + @Response(HttpStatus.BAD_REQUEST, "Invalid month format") + @Response(HttpStatus.INTERNAL_SERVER_ERROR, "Sync operation failed") + async syncMissingEmpType( + @Body() request: { + month: string; + profileType?: "PROFILE" | "PROFILE_EMPLOYEE"; + }, + @Query() dryRun: boolean = false, + @Query() concurrency: number = 5, + @Query() rateLimit: number = 10, + ) { + const { month, profileType = "PROFILE" } = request; + + // Validate month format (YYYY-MM) + const monthRegex = /^\d{4}-\d{2}$/; + if (!monthRegex.test(month)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "รูปแบบเดือนไม่ถูกต้อง ต้องเป็น YYYY-MM"); + } + + // Validate profileType + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + // Validate concurrency + if (concurrency < 1 || concurrency > 20) { + throw new HttpError(HttpStatus.BAD_REQUEST, "concurrency ต้องอยู่ระหว่าง 1 ถึง 20"); + } + + // Validate rateLimit + if (rateLimit < 1 || rateLimit > 50) { + throw new HttpError(HttpStatus.BAD_REQUEST, "rateLimit ต้องอยู่ระหว่าง 1 ถึง 50"); + } + + // Execute sync + const result = await this.keycloakAttributeService.syncMissingEmpTypeByMonth({ + month, + profileType, + dryRun, + concurrency, + rateLimit, + }); + + return new HttpSuccess({ + message: `Sync ${dryRun ? "check " : ""}เสร็จสิ้น`, + ...result, + }); + } } diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 39752b7e..ee8f3413 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -66,7 +66,7 @@ import { import { orgStructureCache } from "../utils/OrgStructureCache"; import { OrgIdMapping, AllOrgMappings, SavePosMasterHistory } from "../interfaces/OrgMapping"; import { OrgPermissionData, NodeLevel } from "../interfaces/OrgTypes"; -import { formatPosMaster, generateLabelName, filterPosMasters } from "../utils/org-formatting"; +import { formatPosMaster, generateLabelName, filterPosMasters, getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; @Route("api/v1/org") @Tags("Organization") @@ -8933,13 +8933,25 @@ export class OrganizationController extends Controller { const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any; // Collect profile update for the selected position + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + if (nextHolderId != null && draftPos.positionIsSelected) { + const _null: any = null; + profileUpdates.set(nextHolderId, { + posMasterNo: draftPosMaster ? (getPosMasterNo(draftPosMaster as PosMaster) ?? _null) : _null, + org: draftPosMaster ? (getOrgFullName(draftPosMaster as PosMaster) ?? _null) : _null, + }); + } // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (nextHolderId != null && draftPos.positionIsSelected && !draftPosMaster?.isSit) { - profileUpdates.set(nextHolderId, { - position: draftPos.positionName, - posTypeId: draftPos.posTypeId, - posLevelId: draftPos.posLevelId, - }); + const existing = profileUpdates.get(nextHolderId) || {}; + existing.position = draftPos.positionName; + existing.posTypeId = draftPos.posTypeId; + existing.posLevelId = draftPos.posLevelId; + existing.positionField = draftPos.positionField ?? null; + existing.posExecutive = (draftPos as any).posExecutive?.posExecutiveName ?? null; + existing.positionArea = draftPos.positionArea ?? null; + existing.positionExecutiveField = draftPos.positionExecutiveField ?? null; + profileUpdates.set(nextHolderId, existing); if (draftPosMaster && draftPosMaster.ancestorDNA) { // Find the selected position from draft positions const selectedPos = diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 801d4b97..ed8fc343 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -15,6 +15,7 @@ import permission from "../interfaces/permission"; import { ProfileEmployee } from "../entities/ProfileEmployee"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { OrgRevision } from "../entities/OrgRevision"; +import { actingPositionService } from "../services/ActingPositionService"; const REDIS_HOST = process.env.REDIS_HOST; const REDIS_PORT = process.env.REDIS_PORT; @@ -254,6 +255,64 @@ export class PermissionController extends Controller { return new HttpSuccess(res); } + /** + * API permission with acting positions + * @summary permission with acting positions (dotnet api) + * @param {string} action action + * @param {string} system authSysId + */ + @Get("dotnet-acting/{action}/{system}") + public async dotnetActing( + @Request() req: RequestWithUser, + @Path() action: string, + @Path() system: string, + ) { + if (!["CREATE", "DELETE", "GET", "LIST", "UPDATE"].includes(action)) { + throw new HttpError(HttpStatus.NOT_FOUND, "Action ไม่ถูกต้อง"); + } + // ดึง privilege ตามปกติ + let privilege = await new permission().Permission(req, system.toLocaleUpperCase(), action); + + // ดึงข้อมูล profile และ orgRevision + let profile: any = await this.profileRepo.findOne({ + select: ["id"], + where: { keycloak: req.user.sub }, + }); + + if (!profile) { + profile = await this.profileEmployeeRepo.findOne({ + select: ["id"], + where: { keycloak: req.user.sub }, + }); + if (!profile) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); + } + } + + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); + + // ดึงข้อมูลตำแหน่งที่รักษาการ + const actingData = await actingPositionService.getActingPositionsWithPrivilege( + profile.id, + orgRevision?.id, + action, + system.toLocaleUpperCase() + ); + + // ส่งค่ากลับเหมือน dotnet endpoint แต่เพิ่ม isAct และ posMasterActs + return new HttpSuccess({ + privilege, + isAct: actingData.isAct, + posMasterActs: actingData.posMasterActs, + }); + } + /** * API permission (dotnet api) * @summary permission (dotnet api) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 75d6c2a0..37393e14 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -39,6 +39,7 @@ import { AuthRole } from "../entities/AuthRole"; import { RequestWithUser } from "../middlewares/user"; import permission from "../interfaces/permission"; import { resolveNodeLevel, setLogDataDiff } from "../interfaces/utils"; +import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import { PosMasterAssign } from "../entities/PosMasterAssign"; import { Assign } from "../entities/Assign"; import { ProfileEmployee } from "../entities/ProfileEmployee"; @@ -1256,7 +1257,15 @@ export class PositionController extends Controller { ) { await new permission().PermissionUpdate(request, "SYS_ORG"); const posMaster = await this.posMasterRepository.findOne({ - relations: ["positions", "orgRevision"], + relations: [ + "positions", + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + ], where: { id: id }, }); if (!posMaster) { @@ -1451,6 +1460,17 @@ export class PositionController extends Controller { }), ); + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + if (posMaster.orgRevision?.orgRevisionIsCurrent == true && posMaster.current_holderId) { + const _profile = await this.profileRepository.findOne({ + where: { id: posMaster.current_holderId }, + }); + if (_profile) { + _profile.posMasterNo = getPosMasterNo(posMaster); + _profile.org = getOrgFullName(posMaster); + await this.profileRepository.save(_profile); + } + } // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (posMaster.orgRevision?.orgRevisionIsCurrent == true && !posMaster.isSit) { const _position = requestBody.positions.find((p) => p.positionIsSelected == true); @@ -1463,6 +1483,10 @@ export class PositionController extends Controller { _profile.position = _position.posDictName ?? _null; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; + _profile.positionField = _position.posDictField ?? _null; + _profile.posExecutive = _position.posExecutiveId ?? _null; + _profile.positionArea = _position.posDictArea ?? _null; + _profile.positionExecutiveField = _position.posDictExecutiveField ?? _null; await this.profileRepository.save(_profile); } } @@ -2387,16 +2411,16 @@ export class PositionController extends Controller { ? "posMaster.orgRootId IN (:...root)" : "posMaster.orgRootId is null" : "1=1", - { root: _data.root } + { root: _data.root }, ) .andWhere( _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? "posMaster.orgChild1Id IN (:...child1)" - // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `posMaster.orgChild1Id is null` + : // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + `posMaster.orgChild1Id is null` : "1=1", - { child1: _data.child1 } + { child1: _data.child1 }, ) .andWhere( _data.child2 != undefined && _data.child2 != null @@ -2427,26 +2451,27 @@ export class PositionController extends Controller { { child4: _data.child4, }, - ) + ); // .andWhere(checkChildConditions) // .andWhere(typeCondition) // .andWhere(revisionCondition); if (body.keyword != null && body.keyword != "") { - query.orWhere( - new Brackets((qb) => { - qb.andWhere( - body.keyword != null && body.keyword != "" - ? body.isAll == false - ? searchShortName - : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` - : "1=1", - ) - .andWhere(checkChildConditions) - .andWhere(typeCondition) - .andWhere(revisionCondition); - }), - ) + query + .orWhere( + new Brackets((qb) => { + qb.andWhere( + body.keyword != null && body.keyword != "" + ? body.isAll == false + ? searchShortName + : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` + : "1=1", + ) + .andWhere(checkChildConditions) + .andWhere(typeCondition) + .andWhere(revisionCondition); + }), + ) .orWhere( new Brackets((qb) => { qb.andWhere( @@ -2955,50 +2980,50 @@ export class PositionController extends Controller { const type0LastPosMasterNo = requestBody.type == 0 ? await this.posMasterRepository.find({ - where: { - orgRootId: requestBody.id, - orgChild1Id: IsNull(), - }, - }) + where: { + orgRootId: requestBody.id, + orgChild1Id: IsNull(), + }, + }) : []; const type1LastPosMasterNo = requestBody.type == 1 ? await this.posMasterRepository.find({ - where: { - orgChild1Id: requestBody.id, - orgChild2Id: IsNull(), - }, - }) + where: { + orgChild1Id: requestBody.id, + orgChild2Id: IsNull(), + }, + }) : []; const type2LastPosMasterNo = requestBody.type == 2 ? await this.posMasterRepository.find({ - where: { - orgChild2Id: requestBody.id, - orgChild3Id: IsNull(), - }, - }) + where: { + orgChild2Id: requestBody.id, + orgChild3Id: IsNull(), + }, + }) : []; const type3LastPosMasterNo = requestBody.type == 3 ? await this.posMasterRepository.find({ - where: { - orgChild3Id: requestBody.id, - orgChild4Id: IsNull(), - }, - }) + where: { + orgChild3Id: requestBody.id, + orgChild4Id: IsNull(), + }, + }) : []; const type4LastPosMasterNo = requestBody.type == 4 ? await this.posMasterRepository.find({ - where: { - orgChild4Id: requestBody.id, - }, - }) + where: { + orgChild4Id: requestBody.id, + }, + }) : []; const allLastPosMasterNo = [ @@ -3793,7 +3818,7 @@ export class PositionController extends Controller { await new permission().PermissionUpdate(request, "SYS_ORG"); const dataMaster = await this.posMasterRepository.findOne({ where: { id: requestBody.posMaster }, - relations: ["positions"], + relations: ["positions", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], }); if (!dataMaster) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); @@ -3825,16 +3850,24 @@ export class PositionController extends Controller { if (_profile) { let _position = await this.positionRepository.findOne({ where: { id: requestBody.position, posMasterId: requestBody.posMaster }, + relations: ["posExecutive"], }); if (_position) { + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + _profile.posMasterNo = getPosMasterNo(dataMaster); + _profile.org = getOrgFullName(dataMaster); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if(!dataMaster.isSit){ + if (!dataMaster.isSit) { _profile.position = _position.positionName; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; - await this.profileRepository.save(_profile); - setLogDataDiff(request, { before, after: _profile }); + _profile.positionField = _position.positionField ?? _null; + _profile.posExecutive = _position.posExecutive?.posExecutiveName ?? _null; + _profile.positionArea = _position.positionArea ?? _null; + _profile.positionExecutiveField = _position.positionExecutiveField ?? _null; } + await this.profileRepository.save(_profile); + setLogDataDiff(request, { before, after: _profile }); } } dataMaster.current_holderId = requestBody.profileId; @@ -5169,9 +5202,9 @@ export class PositionController extends Controller { } /** - * API รายการอัตรากำลัง + * API รายการตำแหน่งติดเงื่อนไข * - * @summary ORG_070 - รายการอัตรากำลัง (ADMIN) #56 + * @summary รายการตำแหน่งติดเงื่อนไข * */ @Post("master/position-condition") @@ -5182,7 +5215,7 @@ export class PositionController extends Controller { id: string; revisionId: string; type: number; - isAll: boolean; + isAll: boolean; // true คือเลือกเฉพาะตำแหน่งติดเงื่อนไข / false คือเลือกตำแหน่งทั้งหมด page: number; pageSize: number; keyword?: string; @@ -5202,7 +5235,7 @@ export class PositionController extends Controller { let level: any = resolveNodeLevel(orgDna); const cannotViewRootPosMaster = - (_data.privilege === "PARENT") || + _data.privilege === "PARENT" || (_data.privilege === "BROTHER" && level > 1) || (_data.privilege === "CHILD" && level > 0) || (_data.privilege === "NORMAL" && level != 0); @@ -5234,46 +5267,46 @@ export class PositionController extends Controller { typeCondition = { ...(cannotViewRootPosMaster ? { orgRootId: null } : { orgRootId: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild1Id: IsNull(), - }; - searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild1Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 1) { typeCondition = { ...(cannotViewChild1PosMaster ? { orgChild1Id: null } : { orgChild1Id: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild2Id: IsNull(), - }; - searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild2Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 2) { typeCondition = { ...(cannotViewChild2PosMaster ? { orgChild2Id: null } : { orgChild2Id: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild3Id: IsNull(), - }; - searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild3Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 3) { typeCondition = { ...(cannotViewChild3PosMaster ? { orgChild3Id: null } : { orgChild3Id: body.id }), }; - if (!body.isAll) { - checkChildConditions = { - orgChild4Id: IsNull(), - }; - searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - } else { - } + // if (!body.isAll) { + // checkChildConditions = { + // orgChild4Id: IsNull(), + // }; + // searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + // } else { + // } } else if (body.type === 4) { typeCondition = { ...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }), @@ -5346,7 +5379,7 @@ export class PositionController extends Controller { (masterId.length > 0 ? { id: In(masterId) } : { posMasterNo: Like(`%${body.keyword}%`) })), - current_holderId: IsNull(), + ...(!body.isAll && { isCondition: true }), }, ]; let [posMaster, total] = await AppDataSource.getRepository(PosMaster) @@ -5415,15 +5448,15 @@ export class PositionController extends Controller { new Brackets((qb) => { qb.andWhere( body.keyword != null && body.keyword != "" - ? body.isAll == false - ? searchShortName - : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` + ? `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` : "1=1", ) .andWhere(checkChildConditions) .andWhere(typeCondition) - .andWhere(revisionCondition) - .andWhere({ current_holderId: IsNull() }); + .andWhere(revisionCondition); + if (!body.isAll) { + qb.andWhere({ isCondition: true }); + } }), ) .orWhere( @@ -5433,8 +5466,10 @@ export class PositionController extends Controller { ) .andWhere(checkChildConditions) .andWhere(typeCondition) - .andWhere(revisionCondition) - .andWhere({ current_holderId: IsNull() }); + .andWhere(revisionCondition); + if (!body.isAll) { + qb.andWhere({ isCondition: true }); + } }), ) .orderBy("orgRoot.orgRootOrder", "ASC") diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 15461d31..dbbecb80 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1679,35 +1679,84 @@ export class ProfileController extends Controller { // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); - // todo: รอข้อสรุป - // const retire_raw = await this.salaryRepo.findOne({ - // where: { - // profileId: id, - // commandCode: In(["12", "15", "16"]), - // }, - // order: { order: "desc" }, - // }); - // if (retire_raw) { - // const startDate = retire_raw.commandDateAffect; + // commandCode ที่ถือว่าออกจากราชการ + const retireCommandCodes = ["12", "15", "16"]; - // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน - // let daysCount = 0; - // if (startDate) { - // const start = new Date(startDate); - // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - // } + // ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ + const salaries = await this.salaryRepo.find({ + where: { profileId: id }, + order: { order: "ASC" }, + }); - // const startDateStr = startDate - // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - // : "-"; + // มีคำสั่งพ้นราชการหรือไม่ + if ( + salaries.length > 0 && + salaries.some((s) => s.commandCode && retireCommandCodes.includes(s.commandCode)) + ) { + // กรองข้อมูลซ้ำตาม commandDateAffect + const uniqueSalaries = salaries.filter( + (item, index, self) => + index === + self.findIndex( + (t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime(), + ), + ); - // retires.push({ - // date: `${startDateStr}`, - // detail: retire_raw.commandName ?? "-", - // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - // }); - // } + // วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ" + for (let i = 0; i < uniqueSalaries.length; i++) { + const current = uniqueSalaries[i]; + + // เป็นคำสั่งออกจากราชการหรือไม่ + if (current.commandCode && retireCommandCodes.includes(current.commandCode)) { + const startDate = current.commandDateAffect; + let endDate: Date | null = null; + let endRecord = null; + + // หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ) + for (let j = i + 1; j < uniqueSalaries.length; j++) { + const next = uniqueSalaries[j]; + if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) { + endDate = next.commandDateAffect; + endRecord = next; + break; + } + } + + // ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน + if (!endDate) { + endDate = currentDate; + } + + // คำนวณจำนวนวัน + let daysCount = 0; + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + } + + // สร้าง detail จาก commandName + remark + const commandName = current.commandName || ""; + const remark = current.remark || ""; + const detail = `${commandName} ${remark}`.trim(); + + // แปลงวันที่เป็น format ไทย + const startDateStr = startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + : "-"; + const endDateStr = endDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) + : "-"; + + retires.push({ + date: `${startDateStr} - ${endDateStr}`, + detail: detail || "-", + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-", + }); + } + } + } // กรณีไม่มีข้อมูล if (retires.length === 0) { @@ -11965,4 +12014,90 @@ export class ProfileController extends Controller { return new HttpSuccess(); } + + /** + * API ข้อมูลทะเบียนประวัติตาม keycloak สำหรับเช็คอินเข้าใช้งานระบบ + * + * @summary ข้อมูลทะเบียนประวัติตาม keycloak สำหรับเช็คอินเข้าใช้งานระบบ + * + */ + @Get("keycloak/position-checkin") + async getProfileByKeycloakForCheckin(@Request() request: { user: Record }) { + const userSub = request.user.sub; + const relations = [ + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + ]; + + const [officerProfile, orgRevisionPublish] = await Promise.all([ + this.profileRepo.findOne({ + where: { keycloak: userSub }, + relations, + }), + this.orgRevisionRepo.findOne({ + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }), + ]); + + if (!orgRevisionPublish) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบแบบร่างโครงสร้าง"); + } + + let profile: any = officerProfile; + let profileType: "OFFICER" | "EMPLOYEE" = "OFFICER"; + + if (!profile) { + profile = await this.profileEmpRepo.findOne({ + where: { keycloak: userSub }, + relations, + }); + profileType = "EMPLOYEE"; + } + + if (!profile) { + if (request.user.role.includes("SUPER_ADMIN")) { + return new HttpSuccess(null); + } + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); + } + + const currentHolder = + profile.current_holders?.find((x: any) => x.orgRevisionId == orgRevisionPublish.id) ?? null; + const root = currentHolder?.orgRoot ?? null; + const child1 = currentHolder?.orgChild1 ?? null; + const child2 = currentHolder?.orgChild2 ?? null; + const child3 = currentHolder?.orgChild3 ?? null; + const child4 = currentHolder?.orgChild4 ?? null; + + const _profile: any = { + profileId: profile.id, + keycloak: profile.keycloak, + prefix: profile.prefix, + avatar: profile.avatar, + profileType, + isProbation: profile.isProbation, + avatarName: profile.avatarName, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + root: root?.orgRootName ?? null, + child1: child1?.orgChild1Name ?? null, + child2: child2?.orgChild2Name ?? null, + child3: child3?.orgChild3Name ?? null, + child4: child4?.orgChild4Name ?? null, + privacyCheckin: profile.privacyCheckin, + privacyUser: profile.privacyUser, + privacyMgt: profile.privacyMgt, + ...(profileType !== "OFFICER" ? { type: profile.employeeClass } : {}), + }; + + return new HttpSuccess(_profile); + } } diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 2a7fba38..8ae134c1 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1950,35 +1950,78 @@ export class ProfileEmployeeController extends Controller { // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); - // todo: รอข้อสรุป - // const retire_raw = await this.salaryRepo.findOne({ - // where: { - // profileEmployeeId: id, - // commandCode: In(["12", "15", "16"]), - // }, - // order: { order: "desc" }, - // }); - // if (retire_raw) { - // const startDate = retire_raw.commandDateAffect; + // commandCode ที่ถือว่าออกจากราชการ + const retireCommandCodes = ["12", "15", "16"]; - // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน - // let daysCount = 0; - // if (startDate) { - // const start = new Date(startDate); - // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - // } + // ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ + const salaries = await this.salaryRepo.find({ + where: { profileEmployeeId: id }, + order: { order: "ASC" }, + }); - // const startDateStr = startDate - // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - // : "-"; + // มีคำสั่งพ้นราชการหรือไม่ + if (salaries.length > 0 && salaries.some((s) => s.commandCode && + retireCommandCodes.includes(s.commandCode))) { + // กรองข้อมูลซ้ำตาม commandDateAffect + const uniqueSalaries = salaries.filter((item, index, self) => + index === self.findIndex((t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime()) + ); - // retires.push({ - // date: `${startDateStr} - ปัจจุบัน`, - // detail: retire_raw.commandName ?? "-", - // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - // }); - // } + // วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ" + for (let i = 0; i < uniqueSalaries.length; i++) { + const current = uniqueSalaries[i]; + + // เป็นคำสั่งออกจากราชการหรือไม่ + if (current.commandCode && retireCommandCodes.includes(current.commandCode)) { + const startDate = current.commandDateAffect; + let endDate: Date | null = null; + let endRecord = null; + + // หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ) + for (let j = i + 1; j < uniqueSalaries.length; j++) { + const next = uniqueSalaries[j]; + if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) { + endDate = next.commandDateAffect; + endRecord = next; + break; + } + } + + // ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน + if (!endDate) { + endDate = currentDate; + } + + // คำนวณจำนวนวัน + let daysCount = 0; + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + } + + // สร้าง detail จาก commandName + remark + const commandName = current.commandName || ""; + const remark = current.remark || ""; + const detail = `${commandName} ${remark}`.trim(); + + // แปลงวันที่เป็น format ไทย + const startDateStr = startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + : "-"; + const endDateStr = endDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) + : "-"; + + retires.push({ + date: `${startDateStr} - ${endDateStr}`, + detail: detail || "-", + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + }); + } + } + } // กรณีไม่มีข้อมูล if (retires.length === 0) { diff --git a/src/controllers/ProfileGovernmentController.ts b/src/controllers/ProfileGovernmentController.ts index 8caaff28..9af40339 100644 --- a/src/controllers/ProfileGovernmentController.ts +++ b/src/controllers/ProfileGovernmentController.ts @@ -6,8 +6,6 @@ import HttpError from "../interfaces/http-error"; import { RequestWithUser } from "../middlewares/user"; import { Profile } from "../entities/Profile"; import { ProfileGovernment, UpdateProfileGovernment } from "../entities/ProfileGovernment"; -import { Position } from "../entities/Position"; -import { PosMaster } from "../entities/PosMaster"; import { calculateAge, calculateGovAge, @@ -15,7 +13,6 @@ import { setLogDataDiff, } from "../interfaces/utils"; import permission from "../interfaces/permission"; -import { OrgRevision } from "../entities/OrgRevision"; import { In } from "typeorm"; @Route("api/v1/org/profile/government") @Tags("ProfileGovernment") @@ -23,9 +20,6 @@ import { In } from "typeorm"; export class ProfileGovernmentHistoryController extends Controller { private profileRepo = AppDataSource.getRepository(Profile); private govRepo = AppDataSource.getRepository(ProfileGovernment); - private positionRepo = AppDataSource.getRepository(Position); - private posMasterRepo = AppDataSource.getRepository(PosMaster); - private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); /** * * @summary ข้อมูลราชการ @@ -33,13 +27,6 @@ export class ProfileGovernmentHistoryController extends Controller { */ @Get("user") public async getGovHistoryUser(@Request() request: { user: Record }) { - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); @@ -51,79 +38,19 @@ export class ProfileGovernmentHistoryController extends Controller { posLevel: true, }, }); - const posMaster = await this.posMasterRepo.findOne({ - where: { - // orgRevision: { - // orgRevisionIsCurrent: true, - // orgRevisionIsDraft: false, - // }, - orgRevisionId: orgRevision?.id, - current_holderId: profile.id, - }, - order: { createdAt: "DESC" }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const position = await this.positionRepo.findOne({ - where: { - positionIsSelected: true, - posMaster: { - // orgRevision: { - // orgRevisionIsCurrent: true, - // orgRevisionIsDraft: false, - // }, - orgRevisionId: orgRevision?.id, - current_holderId: profile.id, - }, - }, - order: { createdAt: "DESC" }, - relations: { - posExecutive: true, - }, - }); - if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - const fullNameParts = [ - posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, - posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, - posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, - posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, - posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, - ]; - const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); - let orgShortName = ""; - if (posMaster != null) { - if (posMaster.orgChild1Id === null) { - orgShortName = posMaster.orgRoot?.orgRootShortName; - } else if (posMaster.orgChild2Id === null) { - orgShortName = posMaster.orgChild1?.orgChild1ShortName; - } else if (posMaster.orgChild3Id === null) { - orgShortName = posMaster.orgChild2?.orgChild2ShortName; - } else if (posMaster.orgChild4Id === null) { - orgShortName = posMaster.orgChild3?.orgChild3ShortName; - } else { - orgShortName = posMaster.orgChild4?.orgChild4ShortName; - } - } - //posMaster?.isSit แก้ไขชั่วคราว + + // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว const data = { - org: org, //สังกัด - positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน + org: record.org ?? null, //สังกัด + positionField: record.positionField ?? null, //สายงาน position: record.position, //ตำแหน่ง posLevel: record.posLevel == null ? null : record.posLevel.posLevelName, //ระดับ - posMasterNo: posMaster == null ? null : `${orgShortName} ${posMaster.posMasterNo}`, //เลขที่ตำแหน่ง + posMasterNo: record.posMasterNo ?? null, //เลขที่ตำแหน่ง posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท - posExecutive: - position == null || position.posExecutive == null || posMaster?.isSit - ? null - : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร + posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร + positionArea: record.positionArea ?? null, //ด้าน/สาขา + positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate), dateRetireLaw: record.dateRetireLaw ?? null, // govAge: record.dateStart == null ? null : calculateAge(record.dateStart), @@ -135,10 +62,10 @@ export class ProfileGovernmentHistoryController extends Controller { govAgePlus: record.govAgePlus, reasonSameDate: record.reasonSameDate, }; - + return new HttpSuccess(data); } - + /** * * @summary ข้อมูลราชการ @@ -150,25 +77,17 @@ export class ProfileGovernmentHistoryController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_OFFICER"); if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); - - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); - + // ค้นหา profile ก่อน const record = await this.profileRepo.findOne({ where: { id: profileId }, relations: ["posType", "posLevel"], }); - + if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile"); } - + // ค้นหา profileSalary แยกต่างหาก const profileWithSalary = await this.profileRepo.findOne({ where: { @@ -201,70 +120,13 @@ export class ProfileGovernmentHistoryController extends Controller { }, }, }); - + // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ record.profileSalary = profileWithSalary?.profileSalary || []; - const posMaster = await this.posMasterRepo.findOne({ - where: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - order: { createdAt: "DESC" }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const position = await this.positionRepo.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - }, - order: { createdAt: "DESC" }, - relations: { - posExecutive: true, - }, - }); - - // if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - const fullNameParts = [ - posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, - posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, - posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, - posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, - posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, - ]; - const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); - let orgShortName = ""; - if (posMaster != null) { - if (posMaster.orgChild1Id === null) { - orgShortName = posMaster.orgRoot?.orgRootShortName ?? ""; - } else if (posMaster.orgChild2Id === null) { - orgShortName = posMaster.orgChild1?.orgChild1ShortName ?? ""; - } else if (posMaster.orgChild3Id === null) { - orgShortName = posMaster.orgChild2?.orgChild2ShortName ?? ""; - } else if (posMaster.orgChild4Id === null) { - orgShortName = posMaster.orgChild3?.orgChild3ShortName ?? ""; - } else { - orgShortName = posMaster.orgChild4?.orgChild4ShortName ?? ""; - } - } + let _OrgLeave: any = []; let _profileSalary: any = null; if (record?.isLeave && record?.profileSalary.length > 0) { - // _OrgLeave = [ - // record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null, - // record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null, - // record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null, - // record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null, - // record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null, - // ]; if (record.leaveType == "RETIRE") { _profileSalary = record?.profileSalary.length > 1 @@ -288,27 +150,23 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); - //posMaster?.isSit แก้ไขชั่วคราว + + // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว const data = { - org: record?.isLeave == false ? org : orgLeave, //สังกัด - positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน + org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด + positionField: record.positionField ?? null, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: record?.isLeave == false - ? posMaster == null - ? null - : `${orgShortName} ${posMaster.posMasterNo}` + ? record.posMasterNo ?? null : _profileSalary != null ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท - posExecutive: - position == null || position.posExecutive == null || posMaster?.isSit - ? null - : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร + posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร + positionArea: record.positionArea ?? null, //ด้าน/สาขา + positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), @@ -320,30 +178,22 @@ export class ProfileGovernmentHistoryController extends Controller { govAgePlus: record?.govAgePlus, reasonSameDate: record?.reasonSameDate, }; - + return new HttpSuccess(data); } - + @Get("admin/{profileId}") public async getGovHistoryAdmin(@Path() profileId: string) { - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); - // ค้นหา profile ก่อน const record = await this.profileRepo.findOne({ where: { id: profileId }, relations: ["posType", "posLevel"], }); - + if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile"); } - + // ค้นหา profileSalary แยกต่างหาก const profileWithSalary = await this.profileRepo.findOne({ where: { @@ -376,70 +226,13 @@ export class ProfileGovernmentHistoryController extends Controller { }, }, }); - + // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ record.profileSalary = profileWithSalary?.profileSalary || []; - const posMaster = await this.posMasterRepo.findOne({ - where: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - order: { createdAt: "DESC" }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const position = await this.positionRepo.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: orgRevision?.id, - current_holderId: profileId, - }, - }, - order: { createdAt: "DESC" }, - relations: { - posExecutive: true, - }, - }); - - // if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - const fullNameParts = [ - posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, - posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, - posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, - posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, - posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, - ]; - const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); - let orgShortName = ""; - if (posMaster != null) { - if (posMaster.orgChild1Id === null) { - orgShortName = posMaster.orgRoot?.orgRootShortName; - } else if (posMaster.orgChild2Id === null) { - orgShortName = posMaster.orgChild1?.orgChild1ShortName; - } else if (posMaster.orgChild3Id === null) { - orgShortName = posMaster.orgChild2?.orgChild2ShortName; - } else if (posMaster.orgChild4Id === null) { - orgShortName = posMaster.orgChild3?.orgChild3ShortName; - } else { - orgShortName = posMaster.orgChild4?.orgChild4ShortName; - } - } + let _OrgLeave: any = []; let _profileSalary: any = null; if (record?.isLeave && record?.profileSalary.length > 0) { - // _OrgLeave = [ - // record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null, - // record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null, - // record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null, - // record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null, - // record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null, - // ]; if (record.leaveType == "RETIRE") { _profileSalary = record?.profileSalary.length > 1 @@ -463,27 +256,23 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); - //posMaster?.isSit แก้ไขชั่วคราว + + // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว const data = { - org: record?.isLeave == false ? org : orgLeave, //สังกัด - positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน + org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด + positionField: record.positionField ?? null, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: record?.isLeave == false - ? posMaster == null - ? null - : `${orgShortName} ${posMaster.posMasterNo}` + ? record.posMasterNo ?? null : _profileSalary != null ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท - posExecutive: - position == null || position.posExecutive == null || posMaster?.isSit - ? null - : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร + posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร + positionArea: record.positionArea ?? null, //ด้าน/สาขา + positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), @@ -496,10 +285,10 @@ export class ProfileGovernmentHistoryController extends Controller { reasonSameDate: record?.reasonSameDate, isLeave: record?.isLeave, }; - + return new HttpSuccess(data); } - + /** * * @summary ประวัติข้อมูลราชการ by keycloak @@ -517,7 +306,7 @@ export class ProfileGovernmentHistoryController extends Controller { }); return new HttpSuccess(record); } - + /** * * @summary ประวัติข้อมูลราชการ @@ -533,12 +322,12 @@ export class ProfileGovernmentHistoryController extends Controller { order: { lastUpdatedAt: "DESC" }, where: { profileId: profileId }, }); - + // record.pop(); - + return new HttpSuccess(record); } - + /** * * @summary แก้ไขข้อมูลราชการ @@ -554,14 +343,14 @@ export class ProfileGovernmentHistoryController extends Controller { const record = await this.profileRepo.findOne({ where: { id: profileId }, }); - + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const before = structuredClone(record); const history = new ProfileGovernment(); - + Object.assign(record, body); Object.assign(history, { ...record, id: undefined }); - + history.profileId = profileId; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; @@ -572,13 +361,14 @@ export class ProfileGovernmentHistoryController extends Controller { history.createdFullName = req.user.name; history.createdAt = new Date(); history.lastUpdatedAt = new Date(); - + await Promise.all([ this.profileRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), this.govRepo.save(history, { data: req }), ]); - + return new HttpSuccess(); } } + \ No newline at end of file diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index c8193750..814f5e89 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -23,6 +23,7 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; import { In, IsNull, LessThan, MoreThan, Not } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { calculateTenure } from "../utils/tenure"; import { TenurePositionOfficer } from "../entities/TenurePositionOfficer"; import { TenureLevelOfficer } from "../entities/TenureLevelOfficer"; import { TenurePositionEmployee } from "../entities/TenurePositionEmployee"; @@ -65,10 +66,12 @@ export class ProfileSalaryController extends Controller { await this.positionOfficerRepo.clear(); const profile = await this.profileRepo.find(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [ x.id, @@ -92,21 +95,18 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionName: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileId: x.id, positionName: calDayDiff.positionName, days_diff: calDayDiff.days_diff, - // Years: (calDayDiff.days_diff / 365.2524).toFixed(4), - // Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - // Days: (calDayDiff.days_diff % 30.4375).toFixed(4), - Years: Math.floor(calDayDiff.days_diff / 365.2524), - Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12), - Days: Math.floor(calDayDiff.days_diff % 30.4375), + Years: year, + Months: month, + Days: day, }; - // data.push(_mapData); - await this.positionOfficerRepo.save(mapData); + data.push(mapData); } - // await this.positionOfficerRepo.save(data); + await this.positionOfficerRepo.save(data); return new HttpSuccess(); } @@ -115,11 +115,13 @@ export class ProfileSalaryController extends Controller { let data: any = []; await this.positionEmployeeRepo.clear(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; const profile = await this.profileEmployeeRepo.find(); for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const position = await AppDataSource.query("CALL GetProfileEmployeeSalaryPosition(?, ?)", [ x.id, @@ -143,21 +145,18 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionName: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileEmployeeId: x.id, positionName: calDayDiff.positionName, days_diff: calDayDiff.days_diff, - // Years: (calDayDiff.days_diff / 365.2524).toFixed(4), - // Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - // Days: (calDayDiff.days_diff % 30.4375).toFixed(4), - Years: Math.floor(calDayDiff.days_diff / 365.2524), - Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12), - Days: Math.floor(calDayDiff.days_diff % 30.4375), + Years: year, + Months: month, + Days: day, }; - // data.push(_mapData); - await this.positionEmployeeRepo.save(mapData); + data.push(mapData); } - // await this.positionEmployeeRepo.save(data); + await this.positionEmployeeRepo.save(data); return new HttpSuccess(); } @@ -167,10 +166,12 @@ export class ProfileSalaryController extends Controller { await this.levelOfficerRepo.clear(); const profile = await this.profileRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const positionLevel = await AppDataSource.query("CALL GetProfileSalaryLevel(?, ?)", [ x.id, @@ -202,20 +203,20 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileId: x.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4), - Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: x.posLevel == null ? 0 : year.toFixed(4), + Months: x.posLevel == null ? 0 : month.toFixed(4), + Days: x.posLevel == null ? 0 : day.toFixed(4), }; - // data.push(_mapData); - await this.levelOfficerRepo.save(mapData); + data.push(mapData); } - // await this.levelOfficerRepo.save(data); + await this.levelOfficerRepo.save(data); return new HttpSuccess(); } @@ -225,10 +226,12 @@ export class ProfileSalaryController extends Controller { await this.levelEmployeeRepo.clear(); const profile = await this.profileEmployeeRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const positionLevel = await AppDataSource.query("CALL GetProfileEmployeeSalaryLevel(?, ?)", [ x.id, @@ -260,26 +263,27 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileEmployeeId: x.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4), - Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: x.posLevel == null ? 0 : year.toFixed(4), + Months: x.posLevel == null ? 0 : month.toFixed(4), + Days: x.posLevel == null ? 0 : day.toFixed(4), }; - // data.push(_mapData); - await this.levelEmployeeRepo.save(mapData); + data.push(mapData); } - // await this.levelEmployeeRepo.save(data); + await this.levelEmployeeRepo.save(data); return new HttpSuccess(); } @Get("TenurePositionExecutiveOfficer") public async cronjobTenureExecutivePositionOfficer() { + let data: any = []; await this.positionExecutiveOfficerRepo.clear(); const profile = await this.profileRepo.find(); const orgRevision = await this.orgRevisionRepository.findOne({ @@ -290,10 +294,12 @@ export class ProfileSalaryController extends Controller { }, }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - let _currentDate = CURRENT_DATE[0].today; + const baseCurrentDate = CURRENT_DATE[0].today; for await (const x of profile) { - if (x?.isLeave) { - _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + // Use leave date if available and valid, otherwise use current date + let _currentDate = baseCurrentDate; + if (x?.isLeave && x.leaveDate) { + _currentDate = Extension.toDateOnlyString(x.leaveDate); } const position = await this.positionRepo.findOne({ where: { @@ -331,16 +337,18 @@ export class ProfileSalaryController extends Controller { }, { days_diff: 0, positionExecutive: null }, ); + const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileId: x.id, positionExecutiveName: calDayDiff.positionExecutive, days_diff: calDayDiff.days_diff, - Years: (calDayDiff.days_diff / 365.2524).toFixed(4), - Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), - Days: (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: year.toFixed(4), + Months: month.toFixed(4), + Days: day.toFixed(4), }; - await this.positionExecutiveOfficerRepo.save(mapData); + data.push(mapData); } + await this.positionExecutiveOfficerRepo.save(data); return new HttpSuccess(); } @@ -602,10 +610,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -641,10 +649,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -675,10 +683,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -739,10 +747,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -782,10 +790,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -819,10 +827,10 @@ export class ProfileSalaryController extends Controller { acc.push(existing); } - // Recalculate year, month, and day - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -911,6 +919,17 @@ export class ProfileSalaryController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(data, { ...body, ...meta }); + // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect + if (["12", "15", "16"].includes(body.commandCode ?? "")) { + data.isGovernment = false; + if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; + } + // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect + else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { + data.isGovernment = true; + if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; + } + const history = new ProfileSalaryHistory(); Object.assign(history, { ...data, id: undefined }); await this.salaryRepo.save(data, { data: req }); @@ -1035,6 +1054,17 @@ export class ProfileSalaryController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(record, body); + // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect + if (["12", "15", "16"].includes(body.commandCode ?? "")) { + record.isGovernment = false; + if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; + } + // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect + else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { + record.isGovernment = true; + if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; + } + Object.assign(history, { ...record, id: undefined }); history.profileSalaryId = salaryId; diff --git a/src/controllers/ProfileSalaryEmployeeController.ts b/src/controllers/ProfileSalaryEmployeeController.ts index 5b87003c..44b93a5d 100644 --- a/src/controllers/ProfileSalaryEmployeeController.ts +++ b/src/controllers/ProfileSalaryEmployeeController.ts @@ -27,6 +27,7 @@ import { Profile } from "../entities/Profile"; import { In, LessThan, IsNull, MoreThan } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { calculateTenure } from "../utils/tenure"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; import Extension from "../interfaces/extension"; @@ -175,9 +176,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -211,9 +213,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -266,9 +269,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -302,9 +306,10 @@ export class ProfileSalaryEmployeeController extends Controller { acc.push(existing); } - existing.year = Math.floor(existing.days / 365.2524); - existing.month = Math.floor((existing.days / 30.4375) % 12); - existing.day = Math.ceil(existing.days % 30.4375); + const { year, month, day } = calculateTenure(existing.days); + existing.year = year; + existing.month = month; + existing.day = day; return acc; }, @@ -398,6 +403,17 @@ export class ProfileSalaryEmployeeController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(data, { ...body, ...meta }); + // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect + if (["12", "15", "16"].includes(body.commandCode ?? "")) { + data.isGovernment = false; + if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; + } + // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect + else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { + data.isGovernment = true; + if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; + } + const history = new ProfileSalaryHistory(); Object.assign(history, { ...data, id: undefined }); const _null: any = null; @@ -532,6 +548,16 @@ export class ProfileSalaryEmployeeController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(record, body); + // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect + if (["12", "15", "16"].includes(body.commandCode ?? "")) { + record.isGovernment = false; + if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null; + } + // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect + else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { + record.isGovernment = true; + if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null; + } Object.assign(history, { ...record, id: undefined }); history.profileSalaryId = salaryId; diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index fc6a9df5..35279fbc 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -133,8 +133,8 @@ export class ProfileSalaryTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` - // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `current_holders.orgChild1Id is null` + : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -545,8 +545,8 @@ export class ProfileSalaryTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` - // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `current_holders.orgChild1Id is null` + : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -1233,6 +1233,13 @@ export class ProfileSalaryTempController extends Controller { isDelete: false, }; Object.assign(data, { ...body, ...meta }); + // if (["12", "15", "16"].includes(body.commandCode ?? "")) { + // data.isGovernment = false; + // if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; + // } else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { + // data.isGovernment = true; + // if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; + // } await this.salaryRepo.save(data, { data: req }); setLogDataDiff(req, { before, after: data }); @@ -1433,10 +1440,10 @@ export class ProfileSalaryTempController extends Controller { profileEmployeeId: x.profileEmployeeId, dateStart: x.commandDateAffect, dateEnd: null, - posNo: `${x.posNoAbb} ${x.posNo}`, + posNo: `${x.posNoAbb ?? ""} ${x.posNo ?? ""}`.trim(), position: x.positionName, commandId: x.commandId, - refCommandNo: `${x.commandNo}/${x.commandYear}`, + refCommandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined, refCommandDate: x.commandDateAffect, status: false, isDeleted: false, @@ -1456,7 +1463,7 @@ export class ProfileSalaryTempController extends Controller { dateStart: x.commandDateAffect, dateEnd: null, commandId: x.commandId, - commandNo: `${x.commandNo}/${x.commandYear}`, + commandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined, commandName: x.commandName ?? "ให้ช่วยราชการ", refCommandDate: x.commandDateSign, refId: x.refId, @@ -1509,6 +1516,16 @@ export class ProfileSalaryTempController extends Controller { const before = structuredClone(record); Object.assign(record, body); + // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect + if (["12", "15", "16"].includes(body.commandCode ?? "")) { + record.isGovernment = false; + if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; + } + // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect + else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { + record.isGovernment = true; + if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; + } record.isEdit = true; record.lastUpdateUserId = req.user.sub; diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index afc686e6..2120dcff 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -814,6 +814,68 @@ export class KeycloakController extends Controller { if (!result) throw new Error("Failed. Cannot remove group to user."); } + @Post("user/reset-password") + @Security("bearerAuth", ["admin"]) + async resetUserPassword(@Request() req: RequestWithUser, @Body() body: { keycloak: string }) { + if (!req.user.role.includes("ADMIN") && !req.user.role.includes("SUPER_ADMIN")) { + throw new HttpError(HttpStatus.FORBIDDEN, "ไม่มีสิทธิ์ดำเนินการ"); + } + + let profile: Profile | ProfileEmployee | null = await this.profileRepo.findOne({ + where: { keycloak: body.keycloak }, + select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"], + }); + + let isEmployee = false; + if (!profile) { + profile = await this.profileEmpRepo.findOne({ + where: { keycloak: body.keycloak, employeeClass: "PERM" }, + select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"], + }); + isEmployee = true; + } + + if (!profile) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้"); + } + + if (!profile.keycloak) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ผู้ใช้ไม่ได้เชื่อมต่อกับ Keycloak"); + } + + let newPassword: string; + const isProduction = process.env.NODE_ENV === "production"; + + if (isProduction && profile.birthDate) { + const _date = new Date(profile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) + .toString() + .padStart(2, "0"); + const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; + newPassword = `${_date}${_month}${_year}`; + } else { + newPassword = "P@ssw0rd"; + } + + const result = await changeUserPassword(profile.keycloak, newPassword); + if (!result) { + throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "ไม่สามารถรีเซ็ตรหัสผ่านได้"); + } + + addLogSequence(req, { + action: "reset-password", + status: "success", + description: `รีเซ็ตรหัสผ่านสำหรับ ${profile.firstName} ${profile.lastName} (${profile.citizenId})`, + }); + + const response = new HttpSuccess(); + response.message = "รีเซ็ตรหัสผ่านสำเร็จ"; + return response; + } + @Get("user/role/{id}") async getRoleUser(@Request() req: RequestWithUser, @Path("id") id: string) { const profile = await this.profileRepo.findOne({ diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index 0609c932..23091552 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -237,11 +237,21 @@ export class WorkflowController extends Controller { savedStates.find((state) => state.id === so.stateId && state.order === 1), ); + // add link sysName = REGISTRY_PROFILE or REGISTRY_PROFILE_EMP + let notiLink = ""; + if (body.sysName === "REGISTRY_PROFILE") { + notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`; + } else if (body.sysName === "REGISTRY_PROFILE_EMP") { + notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`; + } else if (body.sysName === "REGISTRY_IDP") { + notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`; + } + const notificationReceivers = stateOperatorUsersToCreate .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) .map((user) => ({ receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, - notiLink: "", + notiLink: notiLink, })); // ส่ง notification แบบ fire-and-forget @@ -904,14 +914,14 @@ export class WorkflowController extends Controller { const roodIds = [posMasterUser.orgRootId]; const orgRoot = await this.orgRootRepo.findOne({ select: { id: true, isDeputy: true }, - where: { + where: { id: Not(posMasterUser.orgRootId), isDeputy: true, orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }, }); if (orgRoot && orgRoot.isDeputy) { - roodIds.push(orgRoot.id) + roodIds.push(orgRoot.id); } // 2. Pre-calculate conditions - ย้ายออกมาข้างนอก diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index 72a1d505..a875a969 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -140,6 +140,54 @@ export class Profile extends EntityBase { }) posTypeId: string | null; + @Column({ + nullable: true, + comment: "สายงาน", + length: 45, + default: null, + }) + positionField: string; + + @Column({ + nullable: true, + comment: "ตำแหน่งทางการบริหาร", + length: 255, + default: null, + }) + posExecutive?: string; + + @Column({ + nullable: true, + comment: "ด้าน/สาขา", + length: 255, + default: null, + }) + positionArea?: string; + + @Column({ + nullable: true, + comment: "ด้านทางการบริหาร", + length: 255, + default: null, + }) + positionExecutiveField?: string; + + @Column({ + nullable: true, + comment: "เลขที่ตำแหน่ง", + length: 255, + default: null, + }) + posMasterNo?: string; + + @Column({ + nullable: true, + comment: "สังกัด", + type: "text", + default: null, + }) + org?: string; + @Column({ nullable: true, length: 255, diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index b661450c..b59d5e81 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -116,6 +116,34 @@ export async function withRetry( throw lastError; } +/** + * Fetch with timeout + * Aborts request if it takes longer than specified timeout + */ +async function fetchWithTimeout( + url: RequestInfo | URL, + options: RequestInit = {}, + timeout: number = 10000, +): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + clearTimeout(timeoutId); + return response; + } catch (error: any) { + clearTimeout(timeoutId); + if (error.name === "AbortError") { + throw new Error(`Request timeout after ${timeout}ms`); + } + throw error; + } +} + const KC_URL = process.env.KC_URL; const KC_REALMS = process.env.KC_REALMS; const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID; @@ -144,10 +172,12 @@ export function isTokenExpired(token: string, beforeExpire: number = 30) { /** * Get token from keycloak if needed + * Returns null if Keycloak is unavailable */ -export async function getToken() { +export async function getToken(): Promise { if (!KC_CLIENT_ID || !KC_SECRET) { - throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature."); + console.error("[getToken] KC_CLIENT_ID and KC_SECRET are required"); + return null; } if (token && !isTokenExpired(token)) return token; @@ -158,22 +188,35 @@ export async function getToken() { body.append("client_secret", KC_SECRET); body.append("grant_type", "client_credentials"); - const res = await fetch(`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, { - method: "POST", - body: body, - }).catch((e) => console.error(e)); + try { + const res = await fetchWithTimeout( + `${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, + { + method: "POST", + body: body, + }, + 10000, + ); - if (!res) { - throw new Error("Cannot get token from keycloak."); + if (!res.ok) { + console.error(`[getToken] Keycloak token request failed: ${res.status}`); + return null; + } + + const data = (await res.json()) as any; + + if (data && data.access_token) { + token = data.access_token; + console.log(`[getToken] Token refreshed successfully`); + return token; + } + + console.error("[getToken] No access_token in response"); + return null; + } catch (error: any) { + console.error(`[getToken] Failed to get token: ${error.message}`); + return null; } - - const data = (await res.json()) as any; - - if (data && data.access_token) { - token = data.access_token; - } - console.log(`token: ${token}`); - return token; } /** @@ -189,10 +232,16 @@ export async function createUser( opts?: Record, token?: string, ) { + const authToken = token || (await getToken()); + if (!authToken) { + console.error("[createUser] Failed to get Keycloak token"); + return false; + } + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, { // prettier-ignore headers: { - "authorization": `Bearer ${token || await getToken()}`, + "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, method: "POST", @@ -206,7 +255,6 @@ export async function createUser( if (!res) return false; if (!res.ok) { - // return Boolean(console.error("Keycloak Error Response: ", await res.json())); return await res.json(); } @@ -223,10 +271,16 @@ export async function createUser( * @returns user if success, false otherwise. */ export async function getUser(userId: string, token?: string) { + const authToken = token || (await getToken()); + if (!authToken) { + console.error("[getUser] Failed to get Keycloak token"); + return false; + } + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${token || await getToken()}`, + "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, }).catch((e) => console.log("Keycloak Error: ", e)); @@ -245,10 +299,16 @@ export async function getUser(userId: string, token?: string) { * @returns user if success, false otherwise. */ export async function getUserByUsername(citizenId: string, token?: string) { + const authToken = token || (await getToken()); + if (!authToken) { + console.error("[getUserByUsername] Failed to get Keycloak token"); + return false; + } + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${token || await getToken()}`, + "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, }).catch((e) => console.log("Keycloak Error: ", e)); @@ -379,23 +439,38 @@ export async function getUserCountOrg(first = "", max = "", search = "", userIds export async function editUser(userId: string, opts: Record) { const { password, ...rest } = opts; + const token = await getToken(); + if (!token) { + console.error("[editUser] Failed to get Keycloak token"); + return false; + } + + // Get existing user data to preserve other fields + const existingUser = await getUser(userId, token); + if (!existingUser) { + console.error(`[editUser] User ${userId} not found in Keycloak`); + return false; + } + + // Merge existing user data with updated fields + const updatedUser = { + ...existingUser, + ...rest, + credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, + }; + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${await getToken()}`, + "authorization": `Bearer ${token}`, "content-type": `application/json`, }, method: "PUT", - body: JSON.stringify({ - enabled: true, - credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, - ...rest, - }), + body: JSON.stringify(updatedUser), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { - // return Boolean(console.error("Keycloak Error Response: ", await res.json())); return await res.json(); } @@ -419,6 +494,24 @@ export async function updateName( ) { // const { password, ...rest } = opts; + // Get existing user data to preserve other fields + const existingUser = await getUser(userId); + if (!existingUser) { + console.error(`[updateName] User ${userId} not found in Keycloak`); + return false; + } + + // Merge existing user data with updated name fields + const updatedUser = { + ...existingUser, + firstName, + lastName, + attributes: { + ...(existingUser.attributes || {}), + prefix, + }, + }; + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { @@ -426,16 +519,7 @@ export async function updateName( "content-type": `application/json`, }, method: "PUT", - body: JSON.stringify({ - enabled: true, - // credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, - // ...rest, - firstName, - lastName, - attributes: { - prefix, - }, - }), + body: JSON.stringify(updatedUser), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; @@ -486,10 +570,16 @@ export async function enableStatus(userId: string, status: boolean) { * @returns user true if success, false otherwise. */ export async function deleteUser(userId: string, token?: string) { + const authToken = token || (await getToken()); + if (!authToken) { + console.error("[deleteUser] Failed to get Keycloak token"); + return false; + } + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${token || await getToken()}`, + "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, method: "DELETE", @@ -871,10 +961,16 @@ export async function removeUserGroup(userId: string, groupId: string) { // Function to change user password export async function changeUserPassword(userId: string, newPassword: string) { try { + const token = await getToken(); + if (!token) { + console.error("[changeUserPassword] Failed to get Keycloak token"); + return false; + } + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, { // prettier-ignore headers: { - "authorization": `Bearer ${await getToken()}`, + "authorization": `Bearer ${token}`, "content-type": `application/json`, }, method: "PUT", @@ -885,6 +981,15 @@ export async function changeUserPassword(userId: string, newPassword: string) { }), }).catch((e) => console.log("Keycloak Error: ", e)); + if (!res) { + console.error("[changeUserPassword] No response from Keycloak"); + return false; + } + if (!res.ok) { + console.error(`[changeUserPassword] Failed to change password: ${res.status}`); + return false; + } + return true; } catch (error) { console.error("Error changing password:", error); @@ -895,60 +1000,61 @@ export async function changeUserPassword(userId: string, newPassword: string) { // Function to reset password export async function resetPassword(username: string) { try { - // if (!API_KEY || !AUTH_ACCOUNT_SECRET) { - // throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature."); - // } - // const body = new URLSearchParams(); - // body.append("client_id", "gettoken"); - // body.append("client_secret", AUTH_ACCOUNT_SECRET?.toString()); - // body.append("grant_type", "client_credentials"); - // const tokenResponse = await fetch(`${process.env.KC_URL}/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, { - // method: "POST", - // headers: { - // "Content-Type": "application/x-www-form-urlencoded", - // api_key: API_KEY, - // }, - // body: body - // }); - // if (!tokenResponse.ok) { - // throw new Error("Failed to get admin token"); - // } - // const tokenData = await tokenResponse.json(); - // const adminToken = tokenData.access_token; + const token = await getToken(); + if (!token) { + console.error("[resetPassword] Failed to get Keycloak token"); + return false; + } - const users = await fetch( + const users = await fetchWithTimeout( `${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`, { headers: { - authorization: `Bearer ${await getToken()}`, - // "authorization": `Bearer ${adminToken}`, + authorization: `Bearer ${token}`, "content-type": `application/json`, }, }, + 10000, ); + if (!users.ok) { + const errorText = await users.text(); + console.error(`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`); return false; } + const usersData = await users.json(); + + if (!usersData || usersData.length === 0) { + console.error(`[resetPassword] User not found with email: ${username}`); + return false; + } + const userId = usersData[0].id; - const resetResponse = await fetch( + + const resetResponse = await fetchWithTimeout( `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`, { method: "PUT", headers: { Authorization: `Bearer ${await getToken()}`, - // "Authorization": `Bearer ${adminToken}`, "Content-Type": "application/json", }, body: JSON.stringify(["UPDATE_PASSWORD"]), }, + 10000, ); + if (!resetResponse.ok) { + const errorText = await resetResponse.text(); + console.error(`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`); return false; } + + console.log(`[resetPassword] Password reset email sent successfully to: ${username}`); return { message: "Password reset email sent" }; - } catch (error) { - console.error("Error triggering password reset:", error); + } catch (error: any) { + console.error(`[resetPassword] Error triggering password reset: ${error.message}`); return false; } } @@ -958,8 +1064,14 @@ export async function updateUserAttributes( attributes: Record, ): Promise { try { + const token = await getToken(); + if (!token) { + console.error("[updateUserAttributes] Failed to get Keycloak token"); + return false; + } + // Get existing user data to preserve other attributes - const existingUser = await getUser(userId); + const existingUser = await getUser(userId, token); if (!existingUser) { console.error(`User ${userId} not found in Keycloak`); @@ -984,7 +1096,7 @@ export async function updateUserAttributes( const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { headers: { - authorization: `Bearer ${await getToken()}`, + authorization: `Bearer ${token}`, "content-type": "application/json", }, method: "PUT", diff --git a/src/migration/1776308026834-add_position_fields_to_profile.ts b/src/migration/1776308026834-add_position_fields_to_profile.ts new file mode 100644 index 00000000..9b214460 --- /dev/null +++ b/src/migration/1776308026834-add_position_fields_to_profile.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddPositionFieldsToProfile1776308026834 implements MigrationInterface { + name = 'AddPositionFieldsToProfile1776308026834' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionField\` varchar(45) NULL COMMENT 'สายงาน'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`posExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionArea\` varchar(255) NULL COMMENT 'ด้าน/สาขา'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionExecutiveField\` varchar(255) NULL COMMENT 'ด้านทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`posMasterNo\` varchar(255) NULL COMMENT 'เลขที่ตำแหน่ง'`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`org\` text NULL COMMENT 'สังกัด'`); + + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionField\` varchar(45) NULL COMMENT 'สายงาน'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`posExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionArea\` varchar(255) NULL COMMENT 'ด้าน/สาขา'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionExecutiveField\` varchar(255) NULL COMMENT 'ด้านทางการบริหาร'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`posMasterNo\` varchar(255) NULL COMMENT 'เลขที่ตำแหน่ง'`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`org\` text NULL COMMENT 'สังกัด'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`org\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`posMasterNo\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionExecutiveField\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionArea\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`posExecutive\``); + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionField\``); + + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`org\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`posMasterNo\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionExecutiveField\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionArea\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`posExecutive\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionField\``); + } +} diff --git a/src/services/ActingPositionService.ts b/src/services/ActingPositionService.ts new file mode 100644 index 00000000..e5a0b601 --- /dev/null +++ b/src/services/ActingPositionService.ts @@ -0,0 +1,186 @@ +import { AppDataSource } from "../database/data-source"; +import { AuthRoleAttr } from "../entities/AuthRoleAttr"; +import { PosMasterAct } from "../entities/PosMasterAct"; + +export interface ActingPositionData { + isAct: boolean; + posMasterActs: Array<{ + privilege: string | null; + posNo: string | null; + rootDnaId: string | null; + child1DnaId: string | null; + child2DnaId: string | null; + child3DnaId: string | null; + child4DnaId: string | null; + }>; +} + +export interface ActingPositionWithPrivilegeData extends ActingPositionData { + privilege?: string | null; +} + +/** + * Service สำหรับจัดการข้อมูลตำแหน่งที่รักษาการและ privilege + */ +export class ActingPositionService { + private posMasterActRepo = AppDataSource.getRepository(PosMasterAct); + private authRoleAttrRepo = AppDataSource.getRepository(AuthRoleAttr); + + /** + * ดึงข้อมูลตำแหน่งที่รักษาการและ privilege + * + * @param profileId - ID ของ profile ที่ต้องการตรวจสอบ + * @param orgRevisionId - ID ของ orgRevision ปัจจุบัน + * @param action - Action ที่ต้องการตรวจสอบสิทธิ์ (CREATE, DELETE, GET, LIST, UPDATE) + * @param system - System ID ที่ต้องการตรวจสอบสิทธิ์ (authSysId) + * @returns ข้อมูลตำแหน่งที่รักษาการและ privilege + */ + async getActingPositionsWithPrivilege( + profileId: string, + orgRevisionId: string | undefined, + action?: string, + system?: string + ): Promise { + // ดึงข้อมูล posMasterAct โดย join กับ posMaster (ตำแหน่งที่ถูกรักษาการ) + const posMasterActs = await this.posMasterActRepo + .createQueryBuilder("posMasterAct") + .leftJoinAndSelect("posMasterAct.posMaster", "posMaster") + .addSelect([ + "posMaster.authRoleId", // เพิ่มการดึง authRoleId จากตำแหน่งที่ถูกรักษาการ + "posMaster.posMasterNo", // เพิ่มการดึงเลขที่ตำแหน่ง + "posMaster.posMasterNoPrefix", // เพิ่มการดึง prefix ของเลขที่ตำแหน่ง + "posMaster.posMasterNoSuffix" // เพิ่มการดึง suffix ของเลขที่ตำแหน่ง + ]) + .leftJoinAndSelect("posMaster.orgRoot", "orgRoot") + .leftJoinAndSelect("posMaster.orgChild1", "orgChild1") + .leftJoinAndSelect("posMaster.orgChild2", "orgChild2") + .leftJoinAndSelect("posMaster.orgChild3", "orgChild3") + .leftJoinAndSelect("posMaster.orgChild4", "orgChild4") + .leftJoinAndSelect("posMaster.orgRevision", "orgRevision") + .leftJoinAndSelect("posMasterAct.posMasterChild", "posMasterChild") + .leftJoinAndSelect("posMasterChild.current_holder", "profileChild") + .where("profileChild.id = :profileId", { profileId }) + .andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .andWhere("orgRevision.orgRevisionIsCurrent = true") + .andWhere("orgRevision.orgRevisionIsDraft = false") + .getMany(); + + if (posMasterActs.length === 0) { + return { + isAct: false, + posMasterActs: [], + }; + } + + // วนลูปแต่ละ posMasterAct เพื่อดึง privilege ของตำแหน่งที่รักษาการ + const posMasterActsResponse = await Promise.all( + posMasterActs.map(async (act) => { + let privilege: string | null = null; + let privileges: Record = {}; + + if (act.posMaster?.authRoleId) { + // ถ้าระบุ action และ system มา ให้ดึงเฉพาะ privilege ของระบบนั้นๆ + if (action && system) { + const roleAttr = await this.authRoleAttrRepo + .createQueryBuilder("authRoleAttr") + .select(["authRoleAttr.attrPrivilege", "authRoleAttr.attrIsCreate", "authRoleAttr.attrIsDelete", "authRoleAttr.attrIsGet", "authRoleAttr.attrIsList", "authRoleAttr.attrIsUpdate"]) + .where("authRoleAttr.authRoleId = :authRoleId", { + authRoleId: act.posMaster.authRoleId, + }) + .andWhere("authRoleAttr.authSysId = :system", { system }) + .getOne(); + + if (roleAttr) { + // ตรวจสอบสิทธิ์ตาม action + let hasPermission = false; + const actionUpper = action.trim().toUpperCase(); + + switch (actionUpper) { + case "CREATE": + hasPermission = roleAttr.attrIsCreate; + break; + case "DELETE": + hasPermission = roleAttr.attrIsDelete; + break; + case "GET": + hasPermission = roleAttr.attrIsGet; + break; + case "LIST": + hasPermission = roleAttr.attrIsList; + break; + case "UPDATE": + hasPermission = roleAttr.attrIsUpdate; + break; + } + + if (hasPermission) { + privilege = roleAttr.attrPrivilege; + } + } + } else { + // ดึงข้อมูล AuthRoleAttr สำหรับทุกระบบ + const roleAttrs = await this.authRoleAttrRepo + .createQueryBuilder("authRoleAttr") + .select(["authRoleAttr.authSysId", "authRoleAttr.attrPrivilege"]) + .where("authRoleAttr.authRoleId = :authRoleId", { + authRoleId: act.posMaster.authRoleId, + }) + .getMany(); + + privileges = roleAttrs.reduce((acc, attr) => { + acc[attr.authSysId] = attr.attrPrivilege; + return acc; + }, {} as Record); + } + } + + // จัดรูปแบบเลขที่ตำแหน่งตามรูปแบบ shortName ที่ใช้ในระบบ + const holder = act.posMaster; + const posNo = !holder + ? null + : holder.orgChild4 != null + ? `${holder.orgChild4.orgChild4ShortName} ${holder.posMasterNo}` + : holder.orgChild3 != null + ? `${holder.orgChild3.orgChild3ShortName} ${holder.posMasterNo}` + : holder.orgChild2 != null + ? `${holder.orgChild2.orgChild2ShortName} ${holder.posMasterNo}` + : holder.orgChild1 != null + ? `${holder.orgChild1.orgChild1ShortName} ${holder.posMasterNo}` + : holder.orgRoot != null + ? `${holder.orgRoot.orgRootShortName} ${holder.posMasterNo}` + : null; + + return { + posNo: posNo, + privilege: action && system ? privilege : JSON.stringify(privileges), + rootDnaId: act.posMaster?.orgRoot?.ancestorDNA ?? null, + child1DnaId: act.posMaster?.orgChild1?.ancestorDNA ?? null, + child2DnaId: act.posMaster?.orgChild2?.ancestorDNA ?? null, + child3DnaId: act.posMaster?.orgChild3?.ancestorDNA ?? null, + child4DnaId: act.posMaster?.orgChild4?.ancestorDNA ?? null, + }; + }) + ); + + // ถ้าระบุ action และ system มา ให้ดึง privilege ของตำแหน่งแรก + let specificPrivilege: string | null = null; + if (action && system && posMasterActsResponse.length > 0) { + specificPrivilege = posMasterActsResponse[0].privilege; + } + + const response: ActingPositionWithPrivilegeData = { + isAct: true, + posMasterActs: posMasterActsResponse, + }; + + // ถ้าระบุ action และ system มา ให้เพิ่ม privilege เข้าไปใน response ด้วย + if (action && system) { + response.privilege = specificPrivilege ?? null; + } + + return response; + } +} + +// Export singleton instance +export const actingPositionService = new ActingPositionService(); diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index 2b3af9ab..1e0f3f07 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -442,6 +442,223 @@ export class KeycloakAttributeService { } } + /** + * Check if Keycloak user has empty/null empType attribute + * @param keycloakUserId - Keycloak user ID + * @returns Object with isEmpty flag and currentEmpType value + */ + async checkEmpTypeEmpty(keycloakUserId: string): Promise<{ + isEmpty: boolean; + currentEmpType?: string; + }> { + try { + const user = await getUser(keycloakUserId); + + if (!user || !user.attributes) { + return { isEmpty: true }; + } + + const empType = user.attributes.empType?.[0]; + + return { + isEmpty: !empType || empType.trim() === "", + currentEmpType: empType || "", + }; + } catch (error) { + console.error(`[checkEmpTypeEmpty] Error for user ${keycloakUserId}:`, error); + return { isEmpty: true }; // Assume empty on error + } + } + + /** + * Sync profiles with missing empType for a specific month + * @param options - Sync configuration + * @returns Sync results summary + */ + async syncMissingEmpTypeByMonth(options: { + month: string; // "YYYY-MM" format + profileType?: "PROFILE" | "PROFILE_EMPLOYEE"; + dryRun?: boolean; + concurrency?: number; + rateLimit?: number; + }): Promise<{ + month: string; + profileType: string; + totalProfiles: number; + profilesChecked: number; + missingEmpType: number; + syncSuccess: number; + syncFailed: number; + skipped: number; + executionTime: string; + dryRun: boolean; + }> { + const startTime = Date.now(); + const { + month, + profileType = "PROFILE", + dryRun = false, + concurrency = 5, + rateLimit = 10, + } = options; + + const result = { + month, + profileType, + totalProfiles: 0, + profilesChecked: 0, + missingEmpType: 0, + syncSuccess: 0, + syncFailed: 0, + skipped: 0, + executionTime: "", + dryRun, + }; + + let rateLimiter: RateLimiter | null = null; + + try { + // Parse month (YYYY-MM) to date range + const [year, monthNum] = month.split("-").map(Number); + const startDate = new Date(Date.UTC(year, monthNum - 1, 1, 0, 0, 0)); + const endDate = new Date(Date.UTC(year, monthNum, 0, 23, 59, 59, 999)); + + console.log( + `[syncMissingEmpTypeByMonth] Processing ${profileType} for ${month} (${startDate.toISOString()} to ${endDate.toISOString()})`, + ); + + // Initialize rate limiter if rate limiting is enabled + if (rateLimit && rateLimit > 0) { + rateLimiter = new RateLimiter(rateLimit); + console.log(`[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`); + } + + // Select repository based on profile type + const repo = + profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo; + + // Query profiles updated within the month + const profiles = await repo + .createQueryBuilder("p") + .where("p.keycloak IS NOT NULL") + .andWhere("p.keycloak != :empty", { empty: "" }) + .andWhere("p.lastUpdatedAt BETWEEN :start AND :end", { + start: startDate, + end: endDate, + }) + .orderBy("p.lastUpdatedAt", "ASC") + .getMany(); + + result.totalProfiles = profiles.length; + console.log(`[syncMissingEmpTypeByMonth] Found ${profiles.length} profiles to check`); + + if (profiles.length === 0) { + result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`; + return result; + } + + // Process profiles in parallel with concurrency limit + for (let i = 0; i < profiles.length; i += concurrency) { + const batch = profiles.slice(i, i + concurrency); + + await Promise.all( + batch.map(async (profile) => { + // Apply rate limiting if enabled + if (rateLimiter) { + await rateLimiter.throttle(); + } + + const keycloakUserId = profile.keycloak; + if (!keycloakUserId) { + return { + profileId: profile.id, + status: "skipped" as const, + reason: "No keycloak ID", + }; + } + + try { + // Check if empType is empty in Keycloak + const { isEmpty, currentEmpType } = + await this.checkEmpTypeEmpty(keycloakUserId); + + result.profilesChecked++; + + if (!isEmpty) { + result.skipped++; + return { + profileId: profile.id, + status: "skipped" as const, + reason: "empType already exists", + empType: currentEmpType, + }; + } + + result.missingEmpType++; + + if (dryRun) { + return { + profileId: profile.id, + status: "skipped" as const, + reason: "dry run", + wouldSync: true, + }; + } + + // Sync the profile + const success = await withRetry( + async () => + this.syncOnOrganizationChange(profile.id, profileType), + 3, // maxRetries + 1000, // baseDelay + ); + + if (success) { + result.syncSuccess++; + return { + profileId: profile.id, + status: "synced" as const, + }; + } else { + result.syncFailed++; + return { + profileId: profile.id, + status: "failed" as const, + reason: "Sync returned false", + }; + } + } catch (error: any) { + result.syncFailed++; + return { + profileId: profile.id, + status: "failed" as const, + reason: error.message || "Unknown error", + }; + } + }), + ); + + // Log progress every 50 profiles + const completed = Math.min(i + concurrency, profiles.length); + if (completed % 50 === 0 || completed === profiles.length) { + console.log( + `[syncMissingEmpTypeByMonth] Progress: ${completed}/${profiles.length} profiles processed`, + ); + } + } + + result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`; + console.log( + `[syncMissingEmpTypeByMonth] Completed: total=${result.totalProfiles}, checked=${result.profilesChecked}, missing=${result.missingEmpType}, synced=${result.syncSuccess}, failed=${result.syncFailed}, skipped=${result.skipped}, elapsed=${result.executionTime}`, + ); + } catch (error) { + console.error("[syncMissingEmpTypeByMonth] Error:", error); + throw error; + } + + return result; + } + /** * Clear org DNA attributes in Keycloak for given profiles * Sets all org DNA fields to empty strings diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index a8011900..6ba40258 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -3,6 +3,7 @@ import { AppDataSource } from "../database/data-source"; import { Command } from "../entities/Command"; import { chunkArray, commandTypePath } from "../interfaces/utils"; import CallAPI from "../interfaces/call-api"; +import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { PosMaster } from "../entities/PosMaster"; @@ -651,23 +652,33 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { await posMasterAssignRepository.save(newAssigns); } - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (item.next_holderId != null && !item.isSit) { + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + if (item.next_holderId != null) { const profile = await repoProfile.findOne({ where: { id: item.next_holderId == null ? "" : item.next_holderId }, }); - if (profile != null && item.positions.length > 0) { - let position = await item.positions.find((x) => x.positionIsSelected == true); - if (position == null) { - position = await item.positions.find((x) => x.posLevelId == profile?.posLevelId); - if (position == null) { - position = await item.positions.sort((a, b) => a.orderNo - b.orderNo)[0]; - } - } + if (profile != null) { + profile.posMasterNo = getPosMasterNo(item) ?? _null; + profile.org = getOrgFullName(item) ?? _null; - profile.posLevelId = position?.posLevelId ?? _null; - profile.posTypeId = position?.posTypeId ?? _null; - profile.position = position?.positionName ?? _null; + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (!item.isSit && item.positions.length > 0) { + let position = await item.positions.find((x) => x.positionIsSelected == true); + if (position == null) { + position = await item.positions.find((x) => x.posLevelId == profile?.posLevelId); + if (position == null) { + position = await item.positions.sort((a, b) => a.orderNo - b.orderNo)[0]; + } + } + + profile.posLevelId = position?.posLevelId ?? _null; + profile.posTypeId = position?.posTypeId ?? _null; + profile.position = position?.positionName ?? _null; + profile.positionField = position?.positionField ?? _null; + profile.posExecutive = position?.posExecutive?.posExecutiveName ?? _null; + profile.positionArea = position?.positionArea ?? _null; + profile.positionExecutiveField = position?.positionExecutiveField ?? _null; + } await repoProfile.save(profile); } } diff --git a/src/utils/org-formatting.ts b/src/utils/org-formatting.ts index 701fb478..fd61f33b 100644 --- a/src/utils/org-formatting.ts +++ b/src/utils/org-formatting.ts @@ -68,3 +68,47 @@ export function filterPosMasters( ): PosMaster[] { return posMasters.filter((x) => x[childLevelIdKey] == null && x.isDirector === true); } + +/** + * สร้าง orgShortName จาก posMaster (ต้อง load org relations มาก่อน) + */ +export function getOrgShortName(posMaster: PosMaster): string { + if (posMaster.orgChild1Id === null) { + return posMaster.orgRoot?.orgRootShortName ?? ""; + } else if (posMaster.orgChild2Id === null) { + return posMaster.orgChild1?.orgChild1ShortName ?? ""; + } else if (posMaster.orgChild3Id === null) { + return posMaster.orgChild2?.orgChild2ShortName ?? ""; + } else if (posMaster.orgChild4Id === null) { + return posMaster.orgChild3?.orgChild3ShortName ?? ""; + } else { + return posMaster.orgChild4?.orgChild4ShortName ?? ""; + } +} + +/** + * สร้างชื่อสังกัดเต็ม จาก posMaster (join ด้วย \n) + */ +export function getOrgFullName(posMaster: PosMaster): string { + const parts = [ + posMaster.orgChild4?.orgChild4Name, + posMaster.orgChild3?.orgChild3Name, + posMaster.orgChild2?.orgChild2Name, + posMaster.orgChild1?.orgChild1Name, + posMaster.orgRoot?.orgRootName, + ]; + return parts.filter((part) => part !== undefined && part !== null).join("\n"); +} + +/** + * สร้างเลขที่ตำแหน่ง เช่น "กทม. กบ.1234ช" + */ +export function getPosMasterNo(posMaster: PosMaster): string { + const orgShortName = getOrgShortName(posMaster); + const parts = [ + posMaster.posMasterNoPrefix, + posMaster.posMasterNo, + posMaster.posMasterNoSuffix, + ].filter((part) => part !== null && part !== undefined); + return `${orgShortName} ${parts.join('')}`; +} diff --git a/src/utils/tenure.ts b/src/utils/tenure.ts new file mode 100644 index 00000000..577d314b --- /dev/null +++ b/src/utils/tenure.ts @@ -0,0 +1,23 @@ +/** + * คำนวณอายุงานจากจำนวนวันรวม + * @param totalDays จำนวนวันรวม + * @returns { year, month, day } ปี เดือน วัน + */ +export function calculateTenure(totalDays: number) { + // 1. แปลงเป็น year เต็ม + const year = Math.floor(totalDays / 365.2524); + + // 2. วันที่เหลือหลังหัก year ออก + const remainAfterYear = totalDays - year * 365.2524; + + // 3. แปลงเป็น month เต็ม + const month = Math.floor(remainAfterYear / 30.4375); + + // 4. วันที่เหลือหลังหัก month ออก + const remainAfterMonth = remainAfterYear - month * 30.4375; + + // 5. ปัดลง เฉพาะวัน + const day = Math.floor(remainAfterMonth); + + return { year, month, day }; +}