diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 5b324311..1ea05633 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -108,6 +108,8 @@ import { ExecuteOfficerProfileService } from "../services/ExecuteOfficerProfileS import { ExecuteSalaryService } from "../services/ExecuteSalaryService"; import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; +import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; +import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; import { promisify } from "util"; const REDIS_HOST = process.env.REDIS_HOST; const REDIS_PORT = process.env.REDIS_PORT; @@ -3748,6 +3750,14 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API สร้าง ProfileSalary ข้าราชการ + handle leave/กลับเข้าราชการ + * + * Thin wrapper — เรียก ExecuteSalaryLeaveService (C-PM-08, 09, 17, 18, 41, 48) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + * + * @summary API สร้าง ProfileSalary ข้าราชการ + leave/กลับเข้าราชการ + */ @Post("excexute/salary-leave") public async newSalaryAndUpdateLeave( @Request() req: RequestWithUser, @@ -3804,496 +3814,10 @@ export class CommandController extends Controller { }[]; }, ) { - const roleKeycloak = await this.roleKeycloakRepo.findOne({ - where: { name: Like("USER") }, + await new ExecuteSalaryLeaveService().executeSalaryLeave(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - relations: ["commandType"], - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const today = new Date().setHours(0, 0, 0, 0); - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileRepository.findOne({ - where: { id: item.profileId }, - relations: { - roleKeycloaks: true - }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - //ลบตำแหน่งที่รักษาการแทน - const code = _command?.commandType?.code; - if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { - removePostMasterAct(profile.id); - } - //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก - else if (item.resignId && code && ["C-PM-41"].includes(code)) { - const commandResign = await this.commandReciveRepository.findOne({ - where: { refId: item.resignId }, - relations: { command: true }, - }); - const executeDate = commandResign - ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) - : today; - if ( - commandResign && - _command.status !== "REPORTED" && - (_command.status !== "WAITING" || today < executeDate) - ) { - await reOrderCommandRecivesAndDelete(commandResign!.id); - } - } - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const returnWork = await checkReturnCommandType(String(item.commandId)); - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - data.dateGovernment = item.commandDateAffect ?? new Date(); - data.order = dest_item == null ? 1 : dest_item.order + 1; - const meta = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - if (!returnWork) { - Object.assign(data, { ...item, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - } - const _null: any = null; - profile.isLeave = item.isLeave; - profile.leaveReason = item.leaveReason ?? _null; - profile.dateLeave = item.dateLeave ?? _null; - profile.lastUpdateUserId = req.user.sub; - profile.lastUpdateFullName = req.user.name; - profile.lastUpdatedAt = new Date(); - const clearProfile = await checkCommandType(String(item.commandId)); - - //ปั๊มประวัติก่อนลบตำแหน่ง - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - let orgRootRef = null; - let orgChild1Ref = null; - let orgChild2Ref = null; - let orgChild3Ref = null; - let orgChild4Ref = null; - if (curRevision) { - const curPosMaster = await this.posMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: curRevision.id, - }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - orgRootRef = curPosMaster?.orgRoot ?? null; - orgChild1Ref = curPosMaster?.orgChild1 ?? null; - orgChild2Ref = curPosMaster?.orgChild2 ?? null; - orgChild3Ref = curPosMaster?.orgChild3 ?? null; - orgChild4Ref = curPosMaster?.orgChild4 ?? null; - if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { - await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE"); - } - } - - //ลบตำแหน่ง - if (item.isLeave == true) { - await removeProfileInOrganize(profile.id, "OFFICER"); - } - if (clearProfile.status) { - if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { - const delUserKeycloak = await deleteUser(profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // profile.keycloak = _null; - profile.roleKeycloaks = []; - profile.isActive = false; - profile.isDelete = true; - } - } - profile.leaveCommandId = item.commandId ?? _null; - profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - profile.leaveRemark = clearProfile.leaveRemark ?? _null; - profile.leaveDate = item.commandDateAffect ?? _null; - profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // profile.position = _null; - // profile.posTypeId = _null; - // profile.posLevelId = _null; - } - - if (item.isGovernment == true) { - if (returnWork) { - //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ - await removeProfileInOrganize(profile.id, "OFFICER"); - //ปั๊มตำแหน่งใหม่ - // หา posMaster และเช็ค orgRevisionIsCurrent - let posMaster = await this.posMasterRepository.findOne({ - where: { id: item.posmasterId?.toString() }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - } - - if (posMaster) { - const checkPosition = await this.positionRepository.find({ - where: { - posMasterId: posMaster.id, - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - const clearPosition = checkPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.positionRepository.save(clearPosition); - } - posMaster.current_holderId = profile.id; - posMaster.lastUpdatedAt = new Date(); - // posMaster.conditionReason = _null; - // posMaster.isCondition = false; - await this.posMasterRepository.save(posMaster); - - // Match position ตามลำดับ priority: - // Condition 1: match จาก positionId - // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) - // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) - // Fallback: เลือก position แรกใน posMaster - - let positionNew: Position | null = null; - - // ═══════════════════════════════════════════════════════════ - // CONDITION 1: เช็คจาก positionId ตรง - // ═══════════════════════════════════════════════════════════ - if (item.positionId) { - const positionById = await this.positionRepository.findOne({ - where: { - id: item.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }; - - if (item.positionField) { - whereCondition.positionField = item.positionField; - } - if (item.posExecutiveId) { - whereCondition.posExecutiveId = item.posExecutiveId; - } - if (item.positionExecutiveField) { - whereCondition.positionExecutiveField = item.positionExecutiveField; - } - if (item.positionArea) { - whereCondition.positionArea = item.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - order: { orderNo: "ASC" } - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" } - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - - if (positionNew) { - positionNew.positionIsSelected = true; - await this.positionRepository.save(positionNew, { data: req }); - } - await CreatePosMasterHistoryOfficer(posMaster.id, req); - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - } - const newMapProfileSalary = { - profileId: profile.id, - commandId: item.commandId, - positionName: item.positionNameNew ?? null, - positionType: item.posTypeNameNew ?? null, - positionLevel: item.posLevelNameNew ?? null, - amount: item.amount ? item.amount : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - posNo: item.posNoNew, - posNoAbb: item.posNoAbbNew, - orgRoot: item.orgRootNew, - orgChild1: item.orgChild1New, - orgChild2: item.orgChild2New, - orgChild3: item.orgChild3New, - orgChild4: item.orgChild4New, - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - Object.assign(data, { ...newMapProfileSalary, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - await this.salaryRepo.save(data); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history); - profile.leaveReason = _null; - profile.leaveCommandId = _null; - profile.leaveCommandNo = _null; - profile.leaveRemark = _null; - profile.leaveDate = _null; - profile.leaveType = _null; - profile.position = item.positionNameNew ?? _null; - profile.posTypeId = item.positionTypeNew ?? _null; - profile.posLevelId = item.positionLevelNew ?? _null; - } - let userKeycloakId; - const checkUser = await getUserByUsername(profile.citizenId); - //ถ้ายังไม่มี user keycloak ให้สร้างใหม่ - if (checkUser.length == 0) { - let password = profile.citizenId; - if (profile.birthDate != null) { - // const gregorianYear = profile.birthDate.getFullYear() + 543; - - // const formattedDate = - // profile.birthDate.toISOString().slice(8, 10) + - // profile.birthDate.toISOString().slice(5, 7) + - // gregorianYear; - // password = formattedDate; - 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; - password = `${_date}${_month}${_year}`; - } - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak - const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; - userKeycloakId = await createUser(profile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: profile.lastName, - }); - const list = await getRoles(); - let result = false; - if (Array.isArray(list) && userKeycloakId) { - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - } - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - } - //ถ้ามีอยู่แล้วให้ใช้อันเดิม - else { - const rolesData = await getRoleMappings(checkUser[0].id); - if (rolesData) { - const _roleKeycloak = await this.roleKeycloakRepo.find({ - where: { name: In(rolesData.map((x: any) => x.name)) }, - }); - profile.roleKeycloaks = - _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; - } - profile.keycloak = checkUser[0].id; - } - profile.amount = item.amount ?? _null; - profile.amountSpecial = item.amountSpecial ?? _null; - profile.isActive = true; - profile.isDelete = false; - } - await this.profileRepository.save(profile); - - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE", - // ); - // } - - // update user attribute in keycloak - await updateUserAttributes(profile.keycloak ?? "", { - profileId: [profile.id], - prefix: [profile.prefix || ""], - }); - - // Task #2190 - if (code && ["C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - - } - }), - ); - return new HttpSuccess(); } @@ -4337,223 +3861,13 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, - relations: { commandType: true }, + await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const today = new Date().setHours(0, 0, 0, 0); - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileEmployeeRepository.findOne({ - where: { id: item.profileId }, - // relations: ["roleKeycloaks"], - relations: { - roleKeycloaks: true, - posType: true, - posLevel: true, - }, - }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - const code = _command?.commandType?.code; - //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก - if (item.resignId && code && ["C-PM-42"].includes(code)) { - const commandResign = await this.commandReciveRepository.findOne({ - where: { refId: item.resignId }, - relations: { command: true }, - }); - const executeDate = commandResign - ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) - : today; - if ( - commandResign && - _command.status !== "REPORTED" && - (_command.status !== "WAITING" || today < executeDate) - ) { - await reOrderCommandRecivesAndDelete(commandResign!.id); - } - } - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(data, { - ...item, - ...meta, - profileEmployeeId: item.profileId, - profileId: undefined, - }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - data.dateGovernment = item.commandDateAffect ?? meta.createdAt; - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - const _null: any = null; - profile.isLeave = item.isLeave; - profile.leaveReason = item.leaveReason ?? _null; - profile.dateLeave = item.dateLeave ?? _null; - profile.lastUpdateUserId = req.user.sub; - profile.lastUpdateFullName = req.user.name; - profile.lastUpdatedAt = new Date(); - // บันทึกประวัติก่อนลบตำแหน่ง - const clearProfile = await checkCommandType(String(item.commandId)); - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - let orgRootRef = null; - let orgChild1Ref = null; - let orgChild2Ref = null; - let orgChild3Ref = null; - let orgChild4Ref = null; - if (curRevision) { - const curPosMaster = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: curRevision.id, - }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - orgRootRef = curPosMaster?.orgRoot ?? null; - orgChild1Ref = curPosMaster?.orgChild1 ?? null; - orgChild2Ref = curPosMaster?.orgChild2 ?? null; - orgChild3Ref = curPosMaster?.orgChild3 ?? null; - orgChild4Ref = curPosMaster?.orgChild4 ?? null; - if (curPosMaster) { - await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); - } - } - - // ลบตำแหน่ง - if (item.isLeave == true) { - await removeProfileInOrganize(profile.id, "EMPLOYEE"); - } - - if (clearProfile.status) { - if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { - const delUserKeycloak = await deleteUser(profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // profile.keycloak = _null; - profile.roleKeycloaks = []; - profile.isActive = false; - profile.isDelete = true; - } - } - profile.leaveCommandId = item.commandId ?? _null; - profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - profile.leaveRemark = clearProfile.leaveRemark ?? _null; - profile.leaveDate = item.commandDateAffect ?? _null; - profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // profile.position = _null; - // profile.posTypeId = _null; - // profile.posLevelId = _null; - } - await this.profileEmployeeRepository.save(profile); - - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE_EMPLOYEE", - // ); - // } - - // Task #2190 - if (code && ["C-PM-23", "C-PM-43"].includes(code)) { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - } - }), - ); - return new HttpSuccess(); } - /** - * API สร้าง ProfileSalary ข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ - * - * Thin wrapper — เรียก ExecuteSalaryService (C-PM-13, 15, 16) - * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) - * - * @summary API สร้าง ProfileSalary ข้าราชการ (leave/โอน/ช่วยราชการ) - */ @Post("excexute/salary") public async newSalaryAndUpdate( @Request() req: RequestWithUser, diff --git a/src/services/ExecuteSalaryEmployeeLeaveService.ts b/src/services/ExecuteSalaryEmployeeLeaveService.ts new file mode 100644 index 00000000..bfb1af4c --- /dev/null +++ b/src/services/ExecuteSalaryEmployeeLeaveService.ts @@ -0,0 +1,321 @@ +import { Double } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { CommandRecive } from "../entities/CommandRecive"; +import { Command } from "../entities/Command"; +import { checkCommandType, removeProfileInOrganize, setLogDataDiff } from "../interfaces/utils"; +import { reOrderCommandRecivesAndDelete } from "./CommandService"; +import { CreatePosMasterHistoryEmployee } from "./PositionService"; +import { deleteUser } from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-employee-leave + * (C-PM-23, 42, 43 — ลาออก/ยกเลิกลาออก/กลับเข้าราชการ ของลูกจ้าง) + */ +export interface SalaryEmployeeLeaveItem { + profileId: string; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + positionType: string | null; + positionLevel: string | null; + isLeave: boolean; + leaveReason?: string | null; + dateLeave?: Date | string | null; + isGovernment?: boolean | null; + commandId?: string | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + positionExecutive?: string | null; + positionExecutiveField?: string | null; + positionArea?: string | null; + commandNo: string | null; + commandYear: number | null; + posNo: string | null; + posNoAbb: string | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + resignId: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryEmployeeLeaveExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary ลูกจ้าง + handle leave/กลับเข้าราชการ + * + * ใช้กับ commandType: C-PM-23, 42, 43 + * + * - endpoint /org/command/excexute/salary-employee-leave เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryEmployeeAndUpdateLeave ต้นฉบับ + */ +export class ExecuteSalaryEmployeeLeaveService { + private commandRepository = AppDataSource.getRepository(Command); + private commandReciveRepository = AppDataSource.getRepository(CommandRecive); + private profileRepository = AppDataSource.getRepository(Profile); + private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); + private salaryRepo = AppDataSource.getRepository(ProfileSalary); + private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); + private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); + + /** + * ประมวลผลสร้าง ProfileSalary + handle leave ของลูกจ้าง + */ + async executeSalaryEmployeeLeave( + data: SalaryEmployeeLeaveItem[], + ctx: SalaryEmployeeLeaveExecutionContext, + ): Promise { + console.log("[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave"); + console.log("[ExecuteSalaryEmployeeLeaveService] Request body count:", data?.length); + + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.dateLeave = toDate(it.dateLeave); + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + relations: { commandType: true }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + const today = new Date().setHours(0, 0, 0, 0); + await Promise.all( + data.map(async (item) => { + const profile = await this.profileEmployeeRepository.findOne({ + where: { id: item.profileId }, + relations: { + roleKeycloaks: true, + posType: true, + posLevel: true, + }, + }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + const code = _command?.commandType?.code; + //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก + if (item.resignId && code && ["C-PM-42"].includes(code)) { + const commandResign = await this.commandReciveRepository.findOne({ + where: { refId: item.resignId }, + relations: { command: true }, + }); + const executeDate = commandResign + ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) + : today; + if ( + commandResign && + _command.status !== "REPORTED" && + (_command.status !== "WAITING" || today < executeDate) + ) { + await reOrderCommandRecivesAndDelete(commandResign!.id); + } + } + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + const dest_item = await this.salaryRepo.findOne({ + where: { profileEmployeeId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + order: dest_item == null ? 1 : dest_item.order + 1, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + Object.assign(dataSalary, { + ...item, + ...meta, + profileEmployeeId: item.profileId, + profileId: undefined, + }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? meta.createdAt; + await this.salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await this.salaryHistoryRepo.save(history, { data: req }); + + const _null: any = null; + profile.isLeave = item.isLeave; + profile.leaveReason = item.leaveReason ?? _null; + profile.dateLeave = item.dateLeave ?? _null; + profile.lastUpdateUserId = ctx.user.sub; + profile.lastUpdateFullName = ctx.user.name; + profile.lastUpdatedAt = new Date(); + // บันทึกประวัติก่อนลบตำแหน่ง + const clearProfile = await checkCommandType(String(item.commandId)); + const curRevision = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + let orgRootRef = null; + let orgChild1Ref = null; + let orgChild2Ref = null; + let orgChild3Ref = null; + let orgChild4Ref = null; + if (curRevision) { + const curPosMaster = await this.employeePosMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: curRevision.id, + }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + orgRootRef = curPosMaster?.orgRoot ?? null; + orgChild1Ref = curPosMaster?.orgChild1 ?? null; + orgChild2Ref = curPosMaster?.orgChild2 ?? null; + orgChild3Ref = curPosMaster?.orgChild3 ?? null; + orgChild4Ref = curPosMaster?.orgChild4 ?? null; + if (curPosMaster) { + await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); + } + } + + // ลบตำแหน่ง + if (item.isLeave == true) { + await removeProfileInOrganize(profile.id, "EMPLOYEE"); + } + + if (clearProfile.status) { + if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { + const delUserKeycloak = await deleteUser(profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // profile.keycloak = _null; + profile.roleKeycloaks = []; + profile.isActive = false; + profile.isDelete = true; + } + } + profile.leaveCommandId = item.commandId ?? _null; + profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + profile.leaveRemark = clearProfile.leaveRemark ?? _null; + profile.leaveDate = item.commandDateAffect ?? _null; + profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // profile.position = _null; + // profile.posTypeId = _null; + // profile.posLevelId = _null; + } + await this.profileEmployeeRepository.save(profile); + + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE_EMPLOYEE", + // ); + // } + + // Task #2190 + if (code && ["C-PM-23", "C-PM-43"].includes(code)) { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); + } + } + }), + ); + + console.log("[ExecuteSalaryEmployeeLeaveService] executeSalaryEmployeeLeave completed successfully"); + } +} diff --git a/src/services/ExecuteSalaryLeaveService.ts b/src/services/ExecuteSalaryLeaveService.ts new file mode 100644 index 00000000..04d6c32c --- /dev/null +++ b/src/services/ExecuteSalaryLeaveService.ts @@ -0,0 +1,628 @@ +import { Double, In, Like } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { PosMaster } from "../entities/PosMaster"; +import { Position } from "../entities/Position"; +import { RoleKeycloak } from "../entities/RoleKeycloak"; +import { CommandRecive } from "../entities/CommandRecive"; +import { Command } from "../entities/Command"; +import { + checkCommandType, + checkReturnCommandType, + removePostMasterAct, + removeProfileInOrganize, + setLogDataDiff, +} from "../interfaces/utils"; +import { reOrderCommandRecivesAndDelete } from "./CommandService"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; +import { getOrgFullName, getPosMasterNo } from "../utils/org-formatting"; +import { + addUserRoles, + createUser, + deleteUser, + getRoleMappings, + getRoles, + getUserByUsername, + updateUserAttributes, +} from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-leave + * (C-PM-08, 09, 17, 18, 41, 48 — ลาออก/พักราชการ/กลับเข้าราชการ ของข้าราชการ) + */ +export interface SalaryLeaveItem { + profileId: string; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + positionExecutive: string | null; + positionExecutiveField?: string | null; + positionArea?: string | null; + positionType: string | null; + positionLevel: string | null; + isLeave: boolean; + leaveReason?: string | null; + dateLeave?: Date | string | null; + posExecutiveId?: string | null; + positionField?: string | null; + commandId?: string | null; + isGovernment?: boolean | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + commandNo: string | null; + commandYear: number | null; + posNo: string | null; + posNoAbb: string | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + positionId?: string | null; + positionTypeNew?: string | null; + positionLevelNew?: string | null; + positionNameNew?: string | null; + posmasterId?: string | null; + posTypeNameNew?: string | null; + posLevelNameNew?: string | null; + posNoNew?: string | null; + posNoAbbNew?: string | null; + orgRootNew?: string | null; + orgChild1New?: string | null; + orgChild2New?: string | null; + orgChild3New?: string | null; + orgChild4New?: string | null; + resignId?: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryLeaveExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary ข้าราชการ + handle leave/กลับเข้าราชการ + * + * ใช้กับ commandType: C-PM-08, 09, 17, 18, 41, 48 + * + * - endpoint /org/command/excexute/salary-leave เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeave ต้นฉบับ + */ +export class ExecuteSalaryLeaveService { + private commandRepository = AppDataSource.getRepository(Command); + private commandReciveRepository = AppDataSource.getRepository(CommandRecive); + private profileRepository = AppDataSource.getRepository(Profile); + private salaryRepo = AppDataSource.getRepository(ProfileSalary); + private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); + private posMasterRepository = AppDataSource.getRepository(PosMaster); + private positionRepository = AppDataSource.getRepository(Position); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); + private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); + + /** + * ประมวลผลสร้าง ProfileSalary + handle leave/กลับเข้าราชการ ของข้าราชการ + */ + async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise { + console.log("[ExecuteSalaryLeaveService] Starting executeSalaryLeave"); + console.log("[ExecuteSalaryLeaveService] Request body count:", data?.length); + + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.dateLeave = toDate(it.dateLeave); + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + const roleKeycloak = await this.roleKeycloakRepo.findOne({ + where: { name: Like("USER") }, + }); + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + relations: ["commandType"], + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + const today = new Date().setHours(0, 0, 0, 0); + await Promise.all( + data.map(async (item) => { + const profile = await this.profileRepository.findOne({ + where: { id: item.profileId }, + relations: { + roleKeycloaks: true, + }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + //ลบตำแหน่งที่รักษาการแทน + const code = _command?.commandType?.code; + if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { + removePostMasterAct(profile.id); + } + //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก + else if (item.resignId && code && ["C-PM-41"].includes(code)) { + const commandResign = await this.commandReciveRepository.findOne({ + where: { refId: item.resignId }, + relations: { command: true }, + }); + const executeDate = commandResign + ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) + : today; + if ( + commandResign && + _command.status !== "REPORTED" && + (_command.status !== "WAITING" || today < executeDate) + ) { + await reOrderCommandRecivesAndDelete(commandResign!.id); + } + } + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + const returnWork = await checkReturnCommandType(String(item.commandId)); + const dest_item = await this.salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? new Date(); + dataSalary.order = dest_item == null ? 1 : dest_item.order + 1; + const meta = { + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + if (!returnWork) { + Object.assign(dataSalary, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + await this.salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await this.salaryHistoryRepo.save(history, { data: req }); + } + const _null: any = null; + profile.isLeave = item.isLeave; + profile.leaveReason = item.leaveReason ?? _null; + profile.dateLeave = item.dateLeave ?? _null; + profile.lastUpdateUserId = ctx.user.sub; + profile.lastUpdateFullName = ctx.user.name; + profile.lastUpdatedAt = new Date(); + const clearProfile = await checkCommandType(String(item.commandId)); + + //ปั๊มประวัติก่อนลบตำแหน่ง + const curRevision = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + let orgRootRef = null; + let orgChild1Ref = null; + let orgChild2Ref = null; + let orgChild3Ref = null; + let orgChild4Ref = null; + if (curRevision) { + const curPosMaster = await this.posMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: curRevision.id, + }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + orgRootRef = curPosMaster?.orgRoot ?? null; + orgChild1Ref = curPosMaster?.orgChild1 ?? null; + orgChild2Ref = curPosMaster?.orgChild2 ?? null; + orgChild3Ref = curPosMaster?.orgChild3 ?? null; + orgChild4Ref = curPosMaster?.orgChild4 ?? null; + if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { + await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE"); + } + } + + //ลบตำแหน่ง + if (item.isLeave == true) { + await removeProfileInOrganize(profile.id, "OFFICER"); + } + if (clearProfile.status) { + if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { + const delUserKeycloak = await deleteUser(profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // profile.keycloak = _null; + profile.roleKeycloaks = []; + profile.isActive = false; + profile.isDelete = true; + } + } + profile.leaveCommandId = item.commandId ?? _null; + profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + profile.leaveRemark = clearProfile.leaveRemark ?? _null; + profile.leaveDate = item.commandDateAffect ?? _null; + profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // profile.position = _null; + // profile.posTypeId = _null; + // profile.posLevelId = _null; + } + + if (item.isGovernment == true) { + if (returnWork) { + //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ + await removeProfileInOrganize(profile.id, "OFFICER"); + //ปั๊มตำแหน่งใหม่ + // หา posMaster และเช็ค orgRevisionIsCurrent + let posMaster = await this.posMasterRepository.findOne({ + where: { id: item.posmasterId?.toString() }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + posMaster = await this.posMasterRepository.findOne({ + where: { + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + } + + if (posMaster) { + const checkPosition = await this.positionRepository.find({ + where: { + posMasterId: posMaster.id, + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + const clearPosition = checkPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await this.positionRepository.save(clearPosition); + } + posMaster.current_holderId = profile.id; + posMaster.lastUpdatedAt = new Date(); + // posMaster.conditionReason = _null; + // posMaster.isCondition = false; + await this.posMasterRepository.save(posMaster); + + // Match position ตามลำดับ priority: + // Condition 1: match จาก positionId + // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) + // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) + // Fallback: เลือก position แรกใน posMaster + + let positionNew: Position | null = null; + + // ═══════════════════════════════════════════════════════════ + // CONDITION 1: เช็คจาก positionId ตรง + // ═══════════════════════════════════════════════════════════ + if (item.positionId) { + const positionById = await this.positionRepository.findOne({ + where: { + id: item.positionId, + posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง + }, + relations: ["posExecutive"], + }); + + if (positionById) { + positionNew = positionById; + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.positionNameNew, + posTypeId: item.positionTypeNew, + posLevelId: item.positionLevelNew, + }; + + if (item.positionField) { + whereCondition.positionField = item.positionField; + } + if (item.posExecutiveId) { + whereCondition.posExecutiveId = item.posExecutiveId; + } + if (item.positionExecutiveField) { + whereCondition.positionExecutiveField = item.positionExecutiveField; + } + if (item.positionArea) { + whereCondition.positionArea = item.positionArea; + } + + const positionBy7Fields = await this.positionRepository.findOne({ + where: whereCondition, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy7Fields) { + positionNew = positionBy7Fields; + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { + const positionBy3Fields = await this.positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.positionNameNew, + posTypeId: item.positionTypeNew, + posLevelId: item.positionLevelNew, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; + } + } + + // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) + // if (!positionNew) { + // const fallbackPositions = await this.positionRepository.find({ + // where: { + // posMasterId: posMaster.id, + // }, + // relations: ["posExecutive"], + // order: { + // orderNo: "ASC", + // }, + // take: 1, + // }); + + // if (fallbackPositions.length > 0) { + // positionNew = fallbackPositions[0]; + // } + // } + + if (positionNew) { + positionNew.positionIsSelected = true; + await this.positionRepository.save(positionNew, { data: req }); + } + await CreatePosMasterHistoryOfficer(posMaster.id, req); + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); + } + const newMapProfileSalary = { + profileId: profile.id, + commandId: item.commandId, + positionName: item.positionNameNew ?? null, + positionType: item.posTypeNameNew ?? null, + positionLevel: item.posLevelNameNew ?? null, + amount: item.amount ? item.amount : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + posNo: item.posNoNew, + posNoAbb: item.posNoAbbNew, + orgRoot: item.orgRootNew, + orgChild1: item.orgChild1New, + orgChild2: item.orgChild2New, + orgChild3: item.orgChild3New, + orgChild4: item.orgChild4New, + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(dataSalary, { ...newMapProfileSalary, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + await this.salaryRepo.save(dataSalary); + history.profileSalaryId = dataSalary.id; + await this.salaryHistoryRepo.save(history); + profile.leaveReason = _null; + profile.leaveCommandId = _null; + profile.leaveCommandNo = _null; + profile.leaveRemark = _null; + profile.leaveDate = _null; + profile.leaveType = _null; + profile.position = item.positionNameNew ?? _null; + profile.posTypeId = item.positionTypeNew ?? _null; + profile.posLevelId = item.positionLevelNew ?? _null; + } + let userKeycloakId; + const checkUser = await getUserByUsername(profile.citizenId); + //ถ้ายังไม่มี user keycloak ให้สร้างใหม่ + if (checkUser.length == 0) { + let password = profile.citizenId; + if (profile.birthDate != null) { + 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; + password = `${_date}${_month}${_year}`; + } + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak + const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; + userKeycloakId = await createUser(profile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: profile.lastName, + }); + const list = await getRoles(); + let result = false; + if (Array.isArray(list) && userKeycloakId) { + result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + } + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + } + //ถ้ามีอยู่แล้วให้ใช้อันเดิม + else { + const rolesData = await getRoleMappings(checkUser[0].id); + if (rolesData) { + const _roleKeycloak = await this.roleKeycloakRepo.find({ + where: { name: In(rolesData.map((x: any) => x.name)) }, + }); + profile.roleKeycloaks = + _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; + } + profile.keycloak = checkUser[0].id; + } + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + profile.isActive = true; + profile.isDelete = false; + } + await this.profileRepository.save(profile); + + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE", + // ); + // } + + // update user attribute in keycloak + await updateUserAttributes(profile.keycloak ?? "", { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + + // Task #2190 + if (code && ["C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); + } + } + }), + ); + + console.log("[ExecuteSalaryLeaveService] executeSalaryLeave completed successfully"); + } +} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index bf684c77..d84a3eed 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -33,6 +33,8 @@ import { ExecuteOfficerProfileService } from "./ExecuteOfficerProfileService"; import { ExecuteSalaryService } from "./ExecuteSalaryService"; import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService"; +import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService"; +import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -326,11 +328,13 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // ───────────────────────────────────────────────────────────── // Linear Flow - // รับ resultData จาก .NET แล้วเรียก Service ตรงๆ ตาม commandType (ไม่ผ่าน HTTP loopback) - // - ExecuteOfficerProfileService : C-PM-01, 02, 14 (บรรจุ/รับโอน) - // - ExecuteSalaryCurrentService : C-PM-03, 04, 05, 06, 07, 39, 47 (แต่งตั้ง-เลื่อน-ย้าย) - // - ExecuteSalaryEmployeeCurrent : C-PM-22, 24 (ลูกจ้าง ปรับระดับชั้นงาน-ย้าย) - // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) + // รับ resultData จาก .NET แล้วเรียก Service ตรงๆ ตาม commandType (ไม่ผ่าน HTTP loopback) + // - ExecuteOfficerProfileService : C-PM-01, 02, 14 (บรรจุ/รับโอน) + // - ExecuteSalaryCurrentService : C-PM-03, 04, 05, 06, 07, 39, 47 (แต่งตั้ง-เลื่อน-ย้าย) + // - ExecuteSalaryEmployeeCurrentService : C-PM-22, 24 (ลูกจ้าง ปรับระดับชั้นงาน-ย้าย) + // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) + // - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ) + // - ExecuteSalaryEmployeeLeaveService : C-PM-23, 42, 43 (ลูกจ้าง leave) // - คำสั่งอื่น ยังใช้ Circular Flow เดิม // ───────────────────────────────────────────────────────────── const code = command.commandType?.code; @@ -338,7 +342,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise { const isSalaryCurrent = ["C-PM-03", "C-PM-04", "C-PM-05", "C-PM-06", "C-PM-07", "C-PM-39", "C-PM-47"].includes(code); const isSalaryEmployeeCurrent = ["C-PM-22", "C-PM-24"].includes(code); const isSalary = ["C-PM-13", "C-PM-15", "C-PM-16"].includes(code); - const isLinearFlow = isOfficerProfile || isSalaryCurrent || isSalaryEmployeeCurrent || isSalary; + const isSalaryLeave = ["C-PM-08", "C-PM-09", "C-PM-17", "C-PM-18", "C-PM-41", "C-PM-48"].includes(code); + const isSalaryEmployeeLeave = ["C-PM-23", "C-PM-42", "C-PM-43"].includes(code); + const isLinearFlow = + isOfficerProfile || + isSalaryCurrent || + isSalaryEmployeeCurrent || + isSalary || + isSalaryLeave || + isSalaryEmployeeLeave; if (isLinearFlow) { console.log(`[AMQ] Linear Flow (${code})`); @@ -384,6 +396,12 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } else if (isSalary) { await new ExecuteSalaryService().executeSalary(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryService`); + } else if (isSalaryLeave) { + await new ExecuteSalaryLeaveService().executeSalaryLeave(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryLeaveService`); + } else if (isSalaryEmployeeLeave) { + await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeLeaveService`); } } } else {