From b64a8bb26d1d96eeb49dd683f4b387bbbe53339b Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 28 Jan 2026 12:04:49 +0700 Subject: [PATCH 001/178] API Get Profile For Logs @Get("user-logs/{keycloakId}") --- .../OrganizationDotnetController.ts | 144 ++++++++++++++---- 1 file changed, 112 insertions(+), 32 deletions(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 0c8e4d9f..5c5f78f1 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -1201,8 +1201,8 @@ export class OrganizationDotnetController extends Controller { profileType: "OFFICER", positionLeaveName: positionLeaveName, posExecutiveName: position == null || position.posExecutive == null - ? null - : position.posExecutive.posExecutiveName, + ? null + : position.posExecutive.posExecutiveName, oc: oc, }; @@ -1345,9 +1345,9 @@ export class OrganizationDotnetController extends Controller { const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || + profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` : profile.posLevel?.posLevelName ?? null; @@ -1549,9 +1549,9 @@ export class OrganizationDotnetController extends Controller { * ========================================= */ const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || + profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` : profile.posLevel?.posLevelName ?? null; @@ -1808,9 +1808,9 @@ export class OrganizationDotnetController extends Controller { * ========================================= */ const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || + profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` : profile.posLevel?.posLevelName ?? null; @@ -1849,7 +1849,7 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, - + posLevel: profile.posLevel?.posLevelName ?? null, posType: profile.posType?.posTypeName ?? null, posExecutiveName: position?.posExecutive?.posExecutiveName ?? null, @@ -1876,6 +1876,86 @@ export class OrganizationDotnetController extends Controller { return new HttpSuccess(mapProfile); } + /** + * API Get Profile For Logs + * + * @summary API Get Profile For Logs + * + * @param {string} keycloakId keycloakId profile + */ + @Get("user-logs/{keycloakId}") + async UserLogs(@Path() keycloakId: string) { + /* ========================= + * 1. Load profile + * ========================= */ + const profile = await this.profileRepo.findOne({ + where: { keycloak: keycloakId }, + relations: { + current_holders: { + orgRevision: true, + orgRoot: true, + }, + }, + }); + + // Employee + if (!profile) { + const profile = await this.profileEmpRepo.findOne({ + where: { keycloak: keycloakId }, + relations: { + current_holders: { + orgRevision: true, + orgRoot: true, + }, + }, + }); + if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const currentHolder = profile.current_holders?.find( + x => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + const mapProfile = { + profileId: profile.id, + keycloak: profile.keycloak, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + rootId: currentHolder?.orgRootId ?? null, + rootDnaId: currentHolder?.orgRoot?.ancestorDNA ?? null, + }; + + return new HttpSuccess(mapProfile); + } + + /* ========================================= + * 2. current holder + * ========================================= */ + const currentHolder = profile.current_holders?.find( + x => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + /* ========================================= + * 8. map response + * ========================================= */ + const mapProfile = { + profileId: profile.id, + keycloak: profile.keycloak, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + rootId: currentHolder?.orgRootId ?? null, + rootDnaId: currentHolder?.orgRoot?.ancestorDNA ?? null, + }; + + return new HttpSuccess(mapProfile); + } + /** * 3. API Get Profile จาก profile id * @@ -4132,7 +4212,7 @@ export class OrganizationDotnetController extends Controller { // "current_holders.orgChild3", // "current_holders.orgChild4", // ], - relations:{ + relations: { posType: true, posLevel: true, current_holders: { @@ -6887,9 +6967,9 @@ export class OrganizationDotnetController extends Controller { @Post("profile-leave/keycloak") async GetProfileLeaveReportByKeycloakIdAsync( - @Body() body: { - keycloakId: string, - report?: string + @Body() body: { + keycloakId: string, + report?: string } ) { const profile = await this.profileRepo.findOne({ @@ -7002,14 +7082,14 @@ export class OrganizationDotnetController extends Controller { lastName: profile.lastName, citizenId: profile.citizenId, birthDate: profile.birthDate, - retireDate: profile.birthDate - ? calculateRetireLaw(profile.birthDate) + retireDate: profile.birthDate + ? calculateRetireLaw(profile.birthDate) : null, - govAge: profile.dateAppoint - ? `${Extension.CalculateGovAge(profile.dateAppoint, 0, 0)} ปี` + govAge: profile.dateAppoint + ? `${Extension.CalculateGovAge(profile.dateAppoint, 0, 0)} ปี` : null, - age: profile.birthDate - ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") + age: profile.birthDate + ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") : null, dateAppoint: profile.dateAppoint, dateCurrent: new Date(), @@ -7078,9 +7158,9 @@ export class OrganizationDotnetController extends Controller { * ========================================= */ const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || + profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` : profile.posLevel?.posLevelName ?? null; @@ -7142,14 +7222,14 @@ export class OrganizationDotnetController extends Controller { lastName: profile.lastName, citizenId: profile.citizenId, birthDate: profile.birthDate, - retireDate: profile.birthDate - ? calculateRetireLaw(profile.birthDate) + retireDate: profile.birthDate + ? calculateRetireLaw(profile.birthDate) : null, - govAge: profile.dateAppoint - ? `${Extension.CalculateGovAge(profile.dateAppoint, 0, 0)} ปี` + govAge: profile.dateAppoint + ? `${Extension.CalculateGovAge(profile.dateAppoint, 0, 0)} ปี` : null, - age: profile.birthDate - ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") + age: profile.birthDate + ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") : null, dateAppoint: profile.dateAppoint, dateCurrent: new Date(), @@ -7182,7 +7262,7 @@ export class OrganizationDotnetController extends Controller { child3: currentHolder?.orgChild3?.orgChild3Name ?? null, child4: currentHolder?.orgChild4?.orgChild4Name ?? null, oc: oc, - + positions: _positions, educations: _educations, }; From bca25a7a525761c512314d8da7a9c1aac2f921a1 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 28 Jan 2026 13:45:52 +0700 Subject: [PATCH 002/178] feat: optimize detailSuperAdmin API to fix database connection issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ปัญหา: API GET /api/v1/org/super-admin/{id} ทำให้ระบบดับเพราะ N+1 queries - เดิม: >1,000,000 queries (100 orgRoots × 10 children × 10 counts/level) - ใหม่: ~10 queries (query รวมครั้งเดียว + 5 org queries) การเปลี่ยนแปลง: 1. สร้าง OrganizationController-optimized.ts - getPositionCounts(): query posMaster ทั้งหมดครั้งเดียว - สร้าง maps (orgRootMap, orgChild1Map, etc.) สำหรับ lookup - ลด queries จาก 1,000,000+ → ~10 queries 2. เพิ่ม import สำหรับ helper functions ใน OrganizationController.ts - import { getPositionCounts, getCounts, getRootCounts } - ต้อง replace ฟังก์ชัน detailSuperAdmin ด้วย optimized version - ดู OPTIMIZED_FUNCTION.ts สำหรับฟังก์ชันใหม่ ไฟล์ที่เพิ่ม: - src/controllers/OrganizationController-optimized.ts (helper functions) - OPTIMIZED_FUNCTION.ts (optimized function reference) - src/utils/log-memory-store.ts (from earlier log middleware fix) หมายเหตุ: ฟังก์ชัน detailSuperAdmin ใน OrganizationController.ts ยังไม่ถูก replace (ต้องทำ manual) - ดู OPTIMIZED_FUNCTION.ts Co-Authored-By: Claude (glm-4.7) --- OPTIMIZED_FUNCTION.ts | 349 ++++++++++++++++++ .../OrganizationController-optimized.ts | 276 ++++++++++++++ src/controllers/OrganizationController.ts | 1 + src/utils/log-memory-store.ts | 84 +++++ 4 files changed, 710 insertions(+) create mode 100644 OPTIMIZED_FUNCTION.ts create mode 100644 src/controllers/OrganizationController-optimized.ts create mode 100644 src/utils/log-memory-store.ts diff --git a/OPTIMIZED_FUNCTION.ts b/OPTIMIZED_FUNCTION.ts new file mode 100644 index 00000000..e6ede546 --- /dev/null +++ b/OPTIMIZED_FUNCTION.ts @@ -0,0 +1,349 @@ +// This file contains the optimized detailSuperAdmin function +// Replace the function at line 1164 in OrganizationController.ts + + /** + * API รายละเอียดโครงสร้าง + * + * @summary ORG_023 - รายละเอียดโครงสร้าง (ADMIN) #25 + * @optimized ลด N+1 queries จาก 1,000,000+ queries → ~10 queries + */ + @Get("super-admin/{id}") + async detailSuperAdmin(@Path() id: string, @Request() request: RequestWithUser) { + const orgRevision = await this.orgRevisionRepository.findOne({ + where: { id: id }, + }); + if (!orgRevision) return new HttpSuccess([]); + + let rootId: any = null; + if (!request.user.role.includes("SUPER_ADMIN")) { + const profile = await this.profileRepo.findOne({ + where: { + keycloak: request.user.sub, + }, + }); + if (profile == null) return new HttpSuccess([]); + + if (!request.user.role.includes("SUPER_ADMIN")) { + const posMaster = await this.posMasterRepository.findOne({ + where: { + orgRevisionId: id, + current_holderId: profile.id, + }, + }); + if (!posMaster) return new HttpSuccess([]); + + rootId = posMaster.orgRootId; + } + } + + // OPTIMIZED: Get all position counts in ONE query + const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } = + await getPositionCounts(id); + + const orgRootData = await AppDataSource.getRepository(OrgRoot) + .createQueryBuilder("orgRoot") + .where("orgRoot.orgRevisionId = :id", { id }) + .andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", { + rootId: rootId, + }) + .orderBy("orgRoot.orgRootOrder", "ASC") + .getMany(); + + const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id) || null; + const orgChild1Data = + orgRootIds && orgRootIds.length > 0 + ? await AppDataSource.getRepository(OrgChild1) + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() + : []; + + const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; + const orgChild2Data = + orgChild1Ids && orgChild1Ids.length > 0 + ? await AppDataSource.getRepository(OrgChild2) + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() + : []; + + const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; + const orgChild3Data = + orgChild2Ids && orgChild2Ids.length > 0 + ? await AppDataSource.getRepository(OrgChild3) + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() + : []; + + const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; + const orgChild4Data = + orgChild3Ids && orgChild3Ids.length > 0 + ? await AppDataSource.getRepository(OrgChild4) + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() + : []; + + // OPTIMIZED: Build formatted data using pre-calculated counts (no nested queries!) + const formattedData = orgRootData.map((orgRoot) => { + const rootCounts = getCounts(orgRootMap, orgRoot.id); + const rootPosCounts = getRootCounts(rootPosMap, orgRoot.id); + + return { + orgTreeId: orgRoot.id, + orgLevel: 0, + orgName: orgRoot.orgRootName, + orgTreeName: orgRoot.orgRootName, + orgTreeShortName: orgRoot.orgRootShortName, + orgTreeCode: orgRoot.orgRootCode, + orgCode: orgRoot.orgRootCode + "00", + orgTreeRank: orgRoot.orgRootRank, + orgTreeRankSub: orgRoot.orgRootRankSub, + orgRootDnaId: orgRoot.ancestorDNA, + DEPARTMENT_CODE: orgRoot.DEPARTMENT_CODE, + DIVISION_CODE: orgRoot.DIVISION_CODE, + SECTION_CODE: orgRoot.SECTION_CODE, + JOB_CODE: orgRoot.JOB_CODE, + orgTreeOrder: orgRoot.orgRootOrder, + orgTreePhoneEx: orgRoot.orgRootPhoneEx, + orgTreePhoneIn: orgRoot.orgRootPhoneIn, + orgTreeFax: orgRoot.orgRootFax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + isDeputy: orgRoot.isDeputy, + isCommission: orgRoot.isCommission, + responsibility: orgRoot.responsibility, + labelName: + orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, + totalPosition: rootCounts.totalPosition, + totalPositionCurrentUse: rootCounts.totalPositionCurrentUse, + totalPositionCurrentVacant: rootCounts.totalPositionCurrentVacant, + totalPositionNextUse: rootCounts.totalPositionNextUse, + totalPositionNextVacant: rootCounts.totalPositionNextVacant, + totalRootPosition: rootPosCounts.totalRootPosition, + totalRootPositionCurrentUse: rootPosCounts.totalRootPositionCurrentUse, + totalRootPositionCurrentVacant: rootPosCounts.totalRootPositionCurrentVacant, + totalRootPositionNextUse: rootPosCounts.totalRootPositionNextUse, + totalRootPositionNextVacant: rootPosCounts.totalRootPositionNextVacant, + children: orgChild1Data + .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) + .map((orgChild1) => { + const child1Counts = getCounts(orgChild1Map, orgChild1.id); + const child1PosKey = `${orgRoot.id}-${orgChild1.id}`; + const child1PosCounts = getRootCounts(rootPosMap, child1PosKey); + + return { + orgTreeId: orgChild1.id, + orgRootId: orgRoot.id, + orgLevel: 1, + orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild1.orgChild1Name, + orgTreeShortName: orgChild1.orgChild1ShortName, + orgTreeCode: orgChild1.orgChild1Code, + orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code, + orgTreeRank: orgChild1.orgChild1Rank, + orgTreeRankSub: orgChild1.orgChild1RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + DEPARTMENT_CODE: orgChild1.DEPARTMENT_CODE, + DIVISION_CODE: orgChild1.DIVISION_CODE, + SECTION_CODE: orgChild1.SECTION_CODE, + JOB_CODE: orgChild1.JOB_CODE, + orgTreeOrder: orgChild1.orgChild1Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild1.orgChild1PhoneEx, + orgTreePhoneIn: orgChild1.orgChild1PhoneIn, + orgTreeFax: orgChild1.orgChild1Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild1.responsibility, + isOfficer: orgChild1.isOfficer, + isInformation: orgChild1.isInformation, + labelName: + orgChild1.orgChild1Name + + " " + + orgRoot.orgRootCode + + orgChild1.orgChild1Code + + " " + + orgChild1.orgChild1ShortName, + totalPosition: child1Counts.totalPosition, + totalPositionCurrentUse: child1Counts.totalPositionCurrentUse, + totalPositionCurrentVacant: child1Counts.totalPositionCurrentVacant, + totalPositionNextUse: child1Counts.totalPositionNextUse, + totalPositionNextVacant: child1Counts.totalPositionNextVacant, + totalRootPosition: child1PosCounts.totalRootPosition, + totalRootPositionCurrentUse: child1PosCounts.totalRootPositionCurrentUse, + totalRootPositionCurrentVacant: child1PosCounts.totalRootPositionCurrentVacant, + totalRootPositionNextUse: child1PosCounts.totalRootPositionNextUse, + totalRootPositionNextVacant: child1PosCounts.totalRootPositionNextVacant, + children: orgChild2Data + .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) + .map((orgChild2) => { + const child2Counts = getCounts(orgChild2Map, orgChild2.id); + const child2PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}`; + const child2PosCounts = getRootCounts(rootPosMap, child2PosKey); + + return { + orgTreeId: orgChild2.id, + orgRootId: orgChild1.id, + orgLevel: 2, + orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild2.orgChild2Name, + orgTreeShortName: orgChild2.orgChild2ShortName, + orgTreeCode: orgChild2.orgChild2Code, + orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code, + orgTreeRank: orgChild2.orgChild2Rank, + orgTreeRankSub: orgChild2.orgChild2RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + DEPARTMENT_CODE: orgChild2.DEPARTMENT_CODE, + DIVISION_CODE: orgChild2.DIVISION_CODE, + SECTION_CODE: orgChild2.SECTION_CODE, + JOB_CODE: orgChild2.JOB_CODE, + orgTreeOrder: orgChild2.orgChild2Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild2.orgChild2PhoneEx, + orgTreePhoneIn: orgChild2.orgChild2PhoneIn, + orgTreeFax: orgChild2.orgChild2Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild2.responsibility, + labelName: + orgChild2.orgChild2Name + + " " + + orgRoot.orgRootCode + + orgChild2.orgChild2Code + + " " + + orgChild2.orgChild2ShortName, + totalPosition: child2Counts.totalPosition, + totalPositionCurrentUse: child2Counts.totalPositionCurrentUse, + totalPositionCurrentVacant: child2Counts.totalPositionCurrentVacant, + totalPositionNextUse: child2Counts.totalPositionNextUse, + totalPositionNextVacant: child2Counts.totalPositionNextVacant, + totalRootPosition: child2PosCounts.totalRootPosition, + totalRootPositionCurrentUse: child2PosCounts.totalRootPositionCurrentUse, + totalRootPositionCurrentVacant: child2PosCounts.totalRootPositionCurrentVacant, + totalRootPositionNextUse: child2PosCounts.totalRootPositionNextUse, + totalRootPositionNextVacant: child2PosCounts.totalRootPositionNextVacant, + children: orgChild3Data + .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) + .map((orgChild3) => { + const child3Counts = getCounts(orgChild3Map, orgChild3.id); + const child3PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}`; + const child3PosCounts = getRootCounts(rootPosMap, child3PosKey); + + return { + orgTreeId: orgChild3.id, + orgRootId: orgChild2.id, + orgLevel: 3, + orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild3.orgChild3Name, + orgTreeShortName: orgChild3.orgChild3ShortName, + orgTreeCode: orgChild3.orgChild3Code, + orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code, + orgTreeRank: orgChild3.orgChild3Rank, + orgTreeRankSub: orgChild3.orgChild3RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + orgChild3DnaId: orgChild3.ancestorDNA, + DEPARTMENT_CODE: orgChild3.DEPARTMENT_CODE, + DIVISION_CODE: orgChild3.DIVISION_CODE, + SECTION_CODE: orgChild3.SECTION_CODE, + JOB_CODE: orgChild3.JOB_CODE, + orgTreeOrder: orgChild3.orgChild3Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild3.orgChild3PhoneEx, + orgTreePhoneIn: orgChild3.orgChild3PhoneIn, + orgTreeFax: orgChild3.orgChild3Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild3.responsibility, + labelName: + orgChild3.orgChild3Name + + " " + + orgRoot.orgRootCode + + orgChild3.orgChild3Code + + " " + + orgChild3.orgChild3ShortName, + totalPosition: child3Counts.totalPosition, + totalPositionCurrentUse: child3Counts.totalPositionCurrentUse, + totalPositionCurrentVacant: child3Counts.totalPositionCurrentVacant, + totalPositionNextUse: child3Counts.totalPositionNextUse, + totalPositionNextVacant: child3Counts.totalPositionNextVacant, + totalRootPosition: child3PosCounts.totalRootPosition, + totalRootPositionCurrentUse: child3PosCounts.totalRootPositionCurrentUse, + totalRootPositionCurrentVacant: child3PosCounts.totalRootPositionCurrentVacant, + totalRootPositionNextUse: child3PosCounts.totalRootPositionNextUse, + totalRootPositionNextVacant: child3PosCounts.totalRootPositionNextVacant, + children: orgChild4Data + .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) + .map((orgChild4) => { + const child4Counts = getCounts(orgChild4Map, orgChild4.id); + const child4PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}-${orgChild4.id}`; + const child4PosCounts = getRootCounts(rootPosMap, child4PosKey); + + return { + orgTreeId: orgChild4.id, + orgRootId: orgChild3.id, + orgLevel: 4, + orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild4.orgChild4Name, + orgTreeShortName: orgChild4.orgChild4ShortName, + orgTreeCode: orgChild4.orgChild4Code, + orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code, + orgTreeRank: orgChild4.orgChild4Rank, + orgTreeRankSub: orgChild4.orgChild4RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + orgChild3DnaId: orgChild3.ancestorDNA, + orgChild4DnaId: orgChild4.ancestorDNA, + DEPARTMENT_CODE: orgChild4.DEPARTMENT_CODE, + DIVISION_CODE: orgChild4.DIVISION_CODE, + SECTION_CODE: orgChild4.SECTION_CODE, + JOB_CODE: orgChild4.JOB_CODE, + orgTreeOrder: orgChild4.orgChild4Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild4.orgChild4PhoneEx, + orgTreePhoneIn: orgChild4.orgChild4PhoneIn, + orgTreeFax: orgChild4.orgChild4Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild4.responsibility, + labelName: + orgChild4.orgChild4Name + + " " + + orgRoot.orgRootCode + + orgChild4.orgChild4Code + + " " + + orgChild4.orgChild4ShortName, + totalPosition: child4Counts.totalPosition, + totalPositionCurrentUse: child4Counts.totalPositionCurrentUse, + totalPositionCurrentVacant: child4Counts.totalPositionCurrentVacant, + totalPositionNextUse: child4Counts.totalPositionNextUse, + totalPositionNextVacant: child4Counts.totalPositionNextVacant, + totalRootPosition: child4PosCounts.totalRootPosition, + totalRootPositionCurrentUse: child4PosCounts.totalRootPositionCurrentUse, + totalRootPositionCurrentVacant: child4PosCounts.totalRootPositionCurrentVacant, + totalRootPositionNextUse: child4PosCounts.totalRootPositionNextUse, + totalRootPositionNextVacant: child4PosCounts.totalRootPositionNextVacant, + children: [], + }; + }), + }; + }), + }; + }), + }); + }, + ); + + return new HttpSuccess(formattedData); + } diff --git a/src/controllers/OrganizationController-optimized.ts b/src/controllers/OrganizationController-optimized.ts new file mode 100644 index 00000000..a5de09b6 --- /dev/null +++ b/src/controllers/OrganizationController-optimized.ts @@ -0,0 +1,276 @@ +import { AppDataSource } from "../database/data-source"; +import { PosMaster } from "../entities/PosMaster"; + +// Helper function to get aggregated position counts +export async function getPositionCounts(orgRevisionId: string) { + // Query all posMaster data for this revision with aggregation + const rawData = await AppDataSource.getRepository(PosMaster) + .createQueryBuilder("pos") + .select([ + "pos.orgRootId", + "pos.orgChild1Id", + "pos.orgChild2Id", + "pos.orgChild3Id", + "pos.orgChild4Id", + ]) + .where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .getMany(); + + // Helper to check if value is null or empty string + const isNull = (val: any) => val === null || val === ""; + + // Build maps for each level + const orgRootMap = new Map(); + const orgChild1Map = new Map(); + const orgChild2Map = new Map(); + const orgChild3Map = new Map(); + const orgChild4Map = new Map(); + + // Nested maps for root positions (positions at specific levels) + const rootPosMap = new Map(); // key: "rootId", "rootId-child1Id", "rootId-child1Id-child2Id", etc. + + for (const pos of rawData) { + const orgRootId = pos.orgRootId || "NULL"; + const orgChild1Id = pos.orgChild1Id || "NULL"; + const orgChild2Id = pos.orgChild2Id || "NULL"; + const orgChild3Id = pos.orgChild3Id || "NULL"; + const orgChild4Id = pos.orgChild4Id || "NULL"; + + // Level 0 (orgRoot) counts + if (!orgRootMap.has(orgRootId)) { + orgRootMap.set(orgRootId, { + totalPosition: 0, + totalPositionCurrentUse: 0, + totalPositionCurrentVacant: 0, + totalPositionNextUse: 0, + totalPositionNextVacant: 0, + }); + } + const rootCounts = orgRootMap.get(orgRootId); + rootCounts.totalPosition++; + + if (!isNull(pos.current_holderId)) rootCounts.totalPositionCurrentUse++; + else rootCounts.totalPositionCurrentVacant++; + + if (!isNull(pos.next_holderId)) rootCounts.totalPositionNextUse++; + else rootCounts.totalPositionNextVacant++; + + // Level 1 (orgChild1) counts + if (!isNull(pos.orgChild1Id)) { + if (!orgChild1Map.has(pos.orgChild1Id)) { + orgChild1Map.set(pos.orgChild1Id, { + totalPosition: 0, + totalPositionCurrentUse: 0, + totalPositionCurrentVacant: 0, + totalPositionNextUse: 0, + totalPositionNextVacant: 0, + }); + } + const child1Counts = orgChild1Map.get(pos.orgChild1Id); + child1Counts.totalPosition++; + if (!isNull(pos.current_holderId)) child1Counts.totalPositionCurrentUse++; + else child1Counts.totalPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child1Counts.totalPositionNextUse++; + else child1Counts.totalPositionNextVacant++; + } + + // Level 2 (orgChild2) counts + if (!isNull(pos.orgChild2Id)) { + if (!orgChild2Map.has(pos.orgChild2Id)) { + orgChild2Map.set(pos.orgChild2Id, { + totalPosition: 0, + totalPositionCurrentUse: 0, + totalPositionCurrentVacant: 0, + totalPositionNextUse: 0, + totalPositionNextVacant: 0, + }); + } + const child2Counts = orgChild2Map.get(pos.orgChild2Id); + child2Counts.totalPosition++; + if (!isNull(pos.current_holderId)) child2Counts.totalPositionCurrentUse++; + else child2Counts.totalPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child2Counts.totalPositionNextUse++; + else child2Counts.totalPositionNextVacant++; + } + + // Level 3 (orgChild3) counts + if (!isNull(pos.orgChild3Id)) { + if (!orgChild3Map.has(pos.orgChild3Id)) { + orgChild3Map.set(pos.orgChild3Id, { + totalPosition: 0, + totalPositionCurrentUse: 0, + totalPositionCurrentVacant: 0, + totalPositionNextUse: 0, + totalPositionNextVacant: 0, + }); + } + const child3Counts = orgChild3Map.get(pos.orgChild3Id); + child3Counts.totalPosition++; + if (!isNull(pos.current_holderId)) child3Counts.totalPositionCurrentUse++; + else child3Counts.totalPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child3Counts.totalPositionNextUse++; + else child3Counts.totalPositionNextVacant++; + } + + // Level 4 (orgChild4) counts + if (!isNull(pos.orgChild4Id)) { + if (!orgChild4Map.has(pos.orgChild4Id)) { + orgChild4Map.set(pos.orgChild4Id, { + totalPosition: 0, + totalPositionCurrentUse: 0, + totalPositionCurrentVacant: 0, + totalPositionNextUse: 0, + totalPositionNextVacant: 0, + }); + } + const child4Counts = orgChild4Map.get(pos.orgChild4Id); + child4Counts.totalPosition++; + if (!isNull(pos.current_holderId)) child4Counts.totalPositionCurrentUse++; + else child4Counts.totalPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child4Counts.totalPositionNextUse++; + else child4Counts.totalPositionNextVacant++; + } + + // Root position counts (positions at specific hierarchy levels) + // For orgRoot level (all children are null) + const rootLevelKey = orgRootId; + if (!rootPosMap.has(rootLevelKey)) { + rootPosMap.set(rootLevelKey, { + totalRootPosition: 0, + totalRootPositionCurrentUse: 0, + totalRootPositionCurrentVacant: 0, + totalRootPositionNextUse: 0, + totalRootPositionNextVacant: 0, + }); + } + if (isNull(pos.orgChild1Id) && isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) { + const rootLevelCounts = rootPosMap.get(rootLevelKey); + rootLevelCounts.totalRootPosition++; + if (!isNull(pos.current_holderId)) rootLevelCounts.totalRootPositionCurrentUse++; + else rootLevelCounts.totalRootPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) rootLevelCounts.totalRootPositionNextUse++; + else rootLevelCounts.totalRootPositionNextVacant++; + } + + // For orgChild1 level + if (!isNull(pos.orgChild1Id)) { + const child1LevelKey = `${orgRootId}-${pos.orgChild1Id}`; + if (!rootPosMap.has(child1LevelKey)) { + rootPosMap.set(child1LevelKey, { + totalRootPosition: 0, + totalRootPositionCurrentUse: 0, + totalRootPositionCurrentVacant: 0, + totalRootPositionNextUse: 0, + totalRootPositionNextVacant: 0, + }); + } + if (isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) { + const child1LevelCounts = rootPosMap.get(child1LevelKey); + child1LevelCounts.totalRootPosition++; + if (!isNull(pos.current_holderId)) child1LevelCounts.totalRootPositionCurrentUse++; + else child1LevelCounts.totalRootPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child1LevelCounts.totalRootPositionNextUse++; + else child1LevelCounts.totalRootPositionNextVacant++; + } + } + + // For orgChild2 level + if (!isNull(pos.orgChild2Id)) { + const child2LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}`; + if (!rootPosMap.has(child2LevelKey)) { + rootPosMap.set(child2LevelKey, { + totalRootPosition: 0, + totalRootPositionCurrentUse: 0, + totalRootPositionCurrentVacant: 0, + totalRootPositionNextUse: 0, + totalRootPositionNextVacant: 0, + }); + } + if (isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) { + const child2LevelCounts = rootPosMap.get(child2LevelKey); + child2LevelCounts.totalRootPosition++; + if (!isNull(pos.current_holderId)) child2LevelCounts.totalRootPositionCurrentUse++; + else child2LevelCounts.totalRootPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child2LevelCounts.totalRootPositionNextUse++; + else child2LevelCounts.totalRootPositionNextVacant++; + } + } + + // For orgChild3 level + if (!isNull(pos.orgChild3Id)) { + const child3LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${pos.orgChild3Id}`; + if (!rootPosMap.has(child3LevelKey)) { + rootPosMap.set(child3LevelKey, { + totalRootPosition: 0, + totalRootPositionCurrentUse: 0, + totalRootPositionCurrentVacant: 0, + totalRootPositionNextUse: 0, + totalRootPositionNextVacant: 0, + }); + } + if (isNull(pos.orgChild4Id)) { + const child3LevelCounts = rootPosMap.get(child3LevelKey); + child3LevelCounts.totalRootPosition++; + if (!isNull(pos.current_holderId)) child3LevelCounts.totalRootPositionCurrentUse++; + else child3LevelCounts.totalRootPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child3LevelCounts.totalRootPositionNextUse++; + else child3LevelCounts.totalRootPositionNextVacant++; + } + } + + // For orgChild4 level + if (!isNull(pos.orgChild4Id)) { + const child4LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${pos.orgChild3Id}-${pos.orgChild4Id}`; + if (!rootPosMap.has(child4LevelKey)) { + rootPosMap.set(child4LevelKey, { + totalRootPosition: 0, + totalRootPositionCurrentUse: 0, + totalRootPositionCurrentVacant: 0, + totalRootPositionNextUse: 0, + totalRootPositionNextVacant: 0, + }); + } + const child4LevelCounts = rootPosMap.get(child4LevelKey); + child4LevelCounts.totalRootPosition++; + if (!isNull(pos.current_holderId)) child4LevelCounts.totalRootPositionCurrentUse++; + else child4LevelCounts.totalRootPositionCurrentVacant++; + if (!isNull(pos.next_holderId)) child4LevelCounts.totalRootPositionNextUse++; + else child4LevelCounts.totalRootPositionNextVacant++; + } + } + + return { + orgRootMap, + orgChild1Map, + orgChild2Map, + orgChild3Map, + orgChild4Map, + rootPosMap, + }; +} + +// Helper function to get counts from maps with defaults +export function getCounts(map: Map, key: string) { + return ( + map.get(key) || { + totalPosition: 0, + totalPositionCurrentUse: 0, + totalPositionCurrentVacant: 0, + totalPositionNextUse: 0, + totalPositionNextVacant: 0, + } + ); +} + +// Helper function to get root position counts +export function getRootCounts(map: Map, key: string) { + return ( + map.get(key) || { + totalRootPosition: 0, + totalRootPositionCurrentUse: 0, + totalRootPositionCurrentVacant: 0, + totalRootPositionNextUse: 0, + totalRootPositionNextVacant: 0, + } + ); +} diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 3050285d..9730e280 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -46,6 +46,7 @@ import { getRoles, addUserRoles, } from "../keycloak"; +import { getPositionCounts, getCounts, getRootCounts } from "./OrganizationController-optimized"; import { CreatePosMasterHistoryEmployee, CreatePosMasterHistoryOfficer, diff --git a/src/utils/log-memory-store.ts b/src/utils/log-memory-store.ts new file mode 100644 index 00000000..bcaa4bec --- /dev/null +++ b/src/utils/log-memory-store.ts @@ -0,0 +1,84 @@ +import { AppDataSource } from "../database/data-source"; +import { OrgRevision } from "../entities/OrgRevision"; + +interface LogCacheData { + currentRevision: OrgRevision | null; + updatedAt: Date; +} + +class LogMemoryStore { + private cache: LogCacheData = { + currentRevision: null, + updatedAt: new Date(), + }; + private readonly REFRESH_INTERVAL = 5 * 60 * 1000; // 5 นาที + private isRefreshing = false; + private isInitialized = false; + private refreshTimer: NodeJS.Timeout | null = null; + + constructor() { + // ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready + } + + // เริ่มต้น cache หลังจาก TypeORM initialize เสร็จ + initialize() { + if (this.isInitialized) { + console.log("[LogMemoryStore] Already initialized"); + return; + } + + this.isInitialized = true; + this.refreshCache(); + this.refreshTimer = setInterval(() => { + this.refreshCache(); + }, this.REFRESH_INTERVAL); + + console.log("[LogMemoryStore] Initialized with", this.REFRESH_INTERVAL / 1000, "second refresh interval"); + } + + private async refreshCache() { + if (this.isRefreshing) { + console.log("[LogMemoryStore] Already refreshing, skipping..."); + return; + } + + this.isRefreshing = true; + try { + const repoRevision = AppDataSource.getRepository(OrgRevision); + const revision = await repoRevision.findOne({ + where: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + this.cache.currentRevision = revision; + this.cache.updatedAt = new Date(); + console.log( + "[LogMemoryStore] Cache refreshed at", + this.cache.updatedAt.toISOString(), + ); + } catch (error) { + console.error("[LogMemoryStore] Error refreshing cache:", error); + } finally { + this.isRefreshing = false; + } + } + + getCurrentRevision(): OrgRevision | null { + return this.cache.currentRevision; + } + + getLastUpdateTime(): Date { + return this.cache.updatedAt; + } + + // สำหรับ shutdown + destroy() { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + } +} + +export const logMemoryStore = new LogMemoryStore(); From 14c26cce723fc6a81f2ec2ea8584fca8d9195923 Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 28 Jan 2026 14:17:31 +0700 Subject: [PATCH 003/178] add api get profileId --- .../OrganizationDotnetController.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 5c5f78f1..99b90bc5 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -404,6 +404,42 @@ export class OrganizationDotnetController extends Controller { return new HttpSuccess(orgRoot); } + /** + * API Get ProfileId + * + * @summary Get ProfileId + * + */ + @Get("get-profileId") + async getProfileInbox( + @Request() request: { user: Record }, + ) { + let profile: any + //OFF + profile = await this.profileRepo.findOne({ + where: { keycloak: request.user.sub }, + select: { id: true } + }); + //EMP + if (!profile) { + profile = await this.profileEmpRepo.findOne({ + where: { keycloak: request.user.sub }, + select: { id: true } + }); + if (!profile) { + if (request.user.role.includes("SUPER_ADMIN")) { + return new HttpSuccess(null); + } else { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); + } + } + } + const result = { + profileId: profile ? profile.id : null + } + return new HttpSuccess(result); + } + /** * 3. API Get Profile จาก keycloak id * From a194d8594be85a424a42328f8b5845db294aa682 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 28 Jan 2026 16:59:09 +0700 Subject: [PATCH 004/178] fix: connection pool settings --- src/database/data-source.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/database/data-source.ts b/src/database/data-source.ts index db5afc0d..e44252a1 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -47,8 +47,8 @@ export const AppDataSource = new DataSource({ logging: true, // timezone: "Z", entities: - process.env.NODE_ENV !== "production" - ? ["src/entities/**/*.ts"] + process.env.NODE_ENV !== "production" + ? ["src/entities/**/*.ts"] : ["dist/entities/**/*{.ts,.js}"], migrations: process.env.NODE_ENV !== "production" @@ -56,6 +56,16 @@ export const AppDataSource = new DataSource({ : ["dist/migration/**/*{.ts,.js}"], subscribers: [], logger: new MyCustomLogger(), + // Connection pool settings to prevent connection exhaustion + extra: { + connectionLimit: +(process.env.DB_CONNECTION_LIMIT || 50), + maxIdle: +(process.env.DB_MAX_IDLE || 10), + idleTimeout: +(process.env.DB_IDLE_TIMEOUT || 60000), + timezone: "+07:00", // Bangkok timezone (UTC+7) + }, + // TypeORM pool settings + poolSize: +(process.env.DB_POOL_SIZE || 10), + maxQueryExecutionTime: +(process.env.DB_MAX_QUERY_TIME || 3000), }); // export default database; From e068aafe3a59c588b59810df9feb0774f961803c Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 28 Jan 2026 17:22:10 +0700 Subject: [PATCH 005/178] =?UTF-8?q?fix:=20=E0=B9=80=E0=B8=9E=E0=B8=B4?= =?UTF-8?q?=E0=B9=88=E0=B8=A1=20Graceful=20Shutdown=20-=20=E0=B8=9B?= =?UTF-8?q?=E0=B9=89=E0=B8=AD=E0=B8=87=E0=B8=81=E0=B8=B1=E0=B8=99=20connec?= =?UTF-8?q?tion=20in=20app=20file,=20Log=20Mnddleware=20+=20Memory=20Store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 46 ++++++++++++++++++- src/middlewares/logs.ts | 38 +++++----------- src/utils/log-memory-store.ts | 86 ++++++++++++++++++++++++++++++++--- 3 files changed, 137 insertions(+), 33 deletions(-) diff --git a/src/app.ts b/src/app.ts index 7f0e0220..0225f162 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { AppDataSource } from "./database/data-source"; import { RegisterRoutes } from "./routes"; import { OrganizationController } from "./controllers/OrganizationController"; import logMiddleware from "./middlewares/logs"; +import { logMemoryStore } from "./utils/log-memory-store"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; import { DateSerializer } from "./interfaces/date-serializer"; @@ -20,6 +21,9 @@ import { initWebSocket } from "./services/webSocket"; async function main() { await AppDataSource.initialize(); + // Initialize LogMemoryStore after database is ready + logMemoryStore.initialize(); + // Setup custom Date serialization for local timezone DateSerializer.setupDateSerialization(); @@ -93,7 +97,7 @@ async function main() { }); // app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`)); - app.listen( + const server = app.listen( APP_PORT, APP_HOST, () => ( @@ -111,6 +115,46 @@ async function main() { } runMessageQueue(); + + // Graceful Shutdown + const gracefulShutdown = async (signal: string) => { + console.log(`\n[APP] ${signal} received. Starting graceful shutdown...`); + + // Stop accepting new connections + server.close(() => { + console.log("[APP] HTTP server closed"); + }); + + // Force close after timeout + const shutdownTimeout = setTimeout(() => { + console.error("[APP] Forced shutdown after timeout"); + process.exit(1); + }, 30000); // 30 seconds timeout + + try { + // Close database connections + if (AppDataSource.isInitialized) { + await AppDataSource.destroy(); + console.log("[APP] Database connections closed"); + } + + // Destroy LogMemoryStore + logMemoryStore.destroy(); + console.log("[APP] LogMemoryStore destroyed"); + + clearTimeout(shutdownTimeout); + console.log("[APP] Graceful shutdown completed"); + process.exit(0); + } catch (error) { + console.error("[APP] Error during shutdown:", error); + clearTimeout(shutdownTimeout); + process.exit(1); + } + }; + + // Listen for shutdown signals + process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); + process.on("SIGINT", () => gracefulShutdown("SIGINT")); } main(); diff --git a/src/middlewares/logs.ts b/src/middlewares/logs.ts index 70020810..99e7e31a 100644 --- a/src/middlewares/logs.ts +++ b/src/middlewares/logs.ts @@ -1,9 +1,6 @@ import { NextFunction, Request, Response } from "express"; import { Client } from "@elastic/elasticsearch"; -import { AppDataSource } from "../database/data-source"; -import { PosMaster } from "../entities/PosMaster"; -import { OrgRevision } from "../entities/OrgRevision"; -import { Profile } from "../entities/Profile"; +import { logMemoryStore } from "../utils/log-memory-store"; if (!process.env.ELASTICSEARCH_INDEX) { throw new Error("Require ELASTICSEARCH_INDEX to store log."); @@ -27,9 +24,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { if (!req.url.startsWith("/api/")) return next(); let data: any; - const repoPosmaster = AppDataSource.getRepository(PosMaster); - const repoProfile = AppDataSource.getRepository(Profile); - const repoRevision = AppDataSource.getRepository(OrgRevision); const originalJson = res.json; @@ -42,13 +36,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { req.app.locals.logData = {}; - const revision = await repoRevision.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - res.on("finish", async () => { try { if (!req.url.startsWith("/api/")) return; @@ -69,7 +56,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { if (req.url.startsWith("/api/v1/org/profile/")) system = "registry"; if (req.url.startsWith("/api/v1/org/profile-employee/")) system = "registry"; if (req.url.startsWith("/api/v1/org/profile-temp/")) system = "registry"; - + if (req.url.startsWith("/api/v1/org/commandType/admin")) system = "admin"; if (req.url.startsWith("/api/v1/org/commandSys/")) system = "admin"; if (req.url.startsWith("/api/v1/org/commandSalary/")) system = "admin"; @@ -79,16 +66,15 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { if (req.url.startsWith("/api/v1/org/keycloak/")) system = "registry"; const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4; - const profileByKeycloak = await repoProfile.findOne({ - where: { keycloak: req.app.locals.logData.userId }, - }); - const rootId = await repoPosmaster.findOne({ - where: { - current_holderId: profileByKeycloak?.id, - orgRevisionId: revision?.id, - }, - select: ["orgRootId"], - }); + + // Get profile from cache + const profileByKeycloak = await logMemoryStore.getProfileByKeycloak( + req.app.locals.logData.userId, + ); + + // Get rootId from cache + const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id); + // console.log("ancestorDNA:", rootId); if (level === 1 && res.statusCode < 500) return; if (level === 2 && res.statusCode < 400) return; @@ -96,7 +82,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { const obj = { logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info", ip: req.ip, - rootId: rootId?.orgRootId ?? null, + rootId: rootId ?? null, systemName: system, startTimeStamp: timestamp, endTimeStamp: new Date().toISOString(), diff --git a/src/utils/log-memory-store.ts b/src/utils/log-memory-store.ts index bcaa4bec..456c06c8 100644 --- a/src/utils/log-memory-store.ts +++ b/src/utils/log-memory-store.ts @@ -1,17 +1,23 @@ import { AppDataSource } from "../database/data-source"; import { OrgRevision } from "../entities/OrgRevision"; +import { Profile } from "../entities/Profile"; +import { PosMaster } from "../entities/PosMaster"; interface LogCacheData { currentRevision: OrgRevision | null; + profileCache: Map; // keycloak → Profile + rootIdCache: Map; // profileId → rootId updatedAt: Date; } class LogMemoryStore { private cache: LogCacheData = { currentRevision: null, + profileCache: new Map(), + rootIdCache: new Map(), updatedAt: new Date(), }; - private readonly REFRESH_INTERVAL = 5 * 60 * 1000; // 5 นาที + private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes private isRefreshing = false; private isInitialized = false; private refreshTimer: NodeJS.Timeout | null = null; @@ -33,7 +39,11 @@ class LogMemoryStore { this.refreshCache(); }, this.REFRESH_INTERVAL); - console.log("[LogMemoryStore] Initialized with", this.REFRESH_INTERVAL / 1000, "second refresh interval"); + console.log( + "[LogMemoryStore] Initialized with", + this.REFRESH_INTERVAL / 1000, + "second refresh interval", + ); } private async refreshCache() { @@ -44,6 +54,7 @@ class LogMemoryStore { this.isRefreshing = true; try { + // Refresh revision cache const repoRevision = AppDataSource.getRepository(OrgRevision); const revision = await repoRevision.findOne({ where: { @@ -52,11 +63,13 @@ class LogMemoryStore { }, }); this.cache.currentRevision = revision; + + // Clear on-demand caches (they will be rebuilt as needed) + this.cache.profileCache.clear(); + this.cache.rootIdCache.clear(); + this.cache.updatedAt = new Date(); - console.log( - "[LogMemoryStore] Cache refreshed at", - this.cache.updatedAt.toISOString(), - ); + console.log("[LogMemoryStore] Cache refreshed at", this.cache.updatedAt.toISOString()); } catch (error) { console.error("[LogMemoryStore] Error refreshing cache:", error); } finally { @@ -72,6 +85,67 @@ class LogMemoryStore { return this.cache.updatedAt; } + /** + * Get Profile by keycloak ID with caching + */ + async getProfileByKeycloak(keycloak: string): Promise { + // Check cache first + if (this.cache.profileCache.has(keycloak)) { + return this.cache.profileCache.get(keycloak)!; + } + + // Fetch from database + const repoProfile = AppDataSource.getRepository(Profile); + const profile = await repoProfile.findOne({ + where: { keycloak }, + }); + + // Cache the result + if (profile) { + this.cache.profileCache.set(keycloak, profile); + } + + return profile; + } + + /** + * Get RootId by profileId with caching + */ + async getRootIdByProfileId(profileId: string | undefined): Promise { + if (!profileId) return null; + + // Check cache first + if (this.cache.rootIdCache.has(profileId)) { + return this.cache.rootIdCache.get(profileId)!; + } + + // Fetch from database + const repoPosmaster = AppDataSource.getRepository(PosMaster); + const revision = this.getCurrentRevision(); + // + const posMaster = await repoPosmaster.findOne({ + where: { + current_holderId: profileId, + orgRevisionId: revision?.id, + }, + relations: ["orgRoot"], + select: { + orgRoot: { + ancestorDNA: true, + }, + }, + }); + + const rootId = posMaster?.orgRoot?.ancestorDNA ?? null; + + // Cache the result + if (rootId) { + this.cache.rootIdCache.set(profileId, rootId); + } + + return rootId; + } + // สำหรับ shutdown destroy() { if (this.refreshTimer) { From 5dcb59632f66499f0492c70843e91a5b3655e8a5 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 28 Jan 2026 17:43:26 +0700 Subject: [PATCH 006/178] fix: Api GET /super-admin/{id} --- OPTIMIZED_FUNCTION.ts | 349 ------ src/controllers/OrganizationController.ts | 1022 +++++------------ .../OrganizationService.ts} | 32 +- 3 files changed, 295 insertions(+), 1108 deletions(-) delete mode 100644 OPTIMIZED_FUNCTION.ts rename src/{controllers/OrganizationController-optimized.ts => services/OrganizationService.ts} (92%) diff --git a/OPTIMIZED_FUNCTION.ts b/OPTIMIZED_FUNCTION.ts deleted file mode 100644 index e6ede546..00000000 --- a/OPTIMIZED_FUNCTION.ts +++ /dev/null @@ -1,349 +0,0 @@ -// This file contains the optimized detailSuperAdmin function -// Replace the function at line 1164 in OrganizationController.ts - - /** - * API รายละเอียดโครงสร้าง - * - * @summary ORG_023 - รายละเอียดโครงสร้าง (ADMIN) #25 - * @optimized ลด N+1 queries จาก 1,000,000+ queries → ~10 queries - */ - @Get("super-admin/{id}") - async detailSuperAdmin(@Path() id: string, @Request() request: RequestWithUser) { - const orgRevision = await this.orgRevisionRepository.findOne({ - where: { id: id }, - }); - if (!orgRevision) return new HttpSuccess([]); - - let rootId: any = null; - if (!request.user.role.includes("SUPER_ADMIN")) { - const profile = await this.profileRepo.findOne({ - where: { - keycloak: request.user.sub, - }, - }); - if (profile == null) return new HttpSuccess([]); - - if (!request.user.role.includes("SUPER_ADMIN")) { - const posMaster = await this.posMasterRepository.findOne({ - where: { - orgRevisionId: id, - current_holderId: profile.id, - }, - }); - if (!posMaster) return new HttpSuccess([]); - - rootId = posMaster.orgRootId; - } - } - - // OPTIMIZED: Get all position counts in ONE query - const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } = - await getPositionCounts(id); - - const orgRootData = await AppDataSource.getRepository(OrgRoot) - .createQueryBuilder("orgRoot") - .where("orgRoot.orgRevisionId = :id", { id }) - .andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", { - rootId: rootId, - }) - .orderBy("orgRoot.orgRootOrder", "ASC") - .getMany(); - - const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id) || null; - const orgChild1Data = - orgRootIds && orgRootIds.length > 0 - ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() - : []; - - const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; - const orgChild2Data = - orgChild1Ids && orgChild1Ids.length > 0 - ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() - : []; - - const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; - const orgChild3Data = - orgChild2Ids && orgChild2Ids.length > 0 - ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() - : []; - - const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; - const orgChild4Data = - orgChild3Ids && orgChild3Ids.length > 0 - ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() - : []; - - // OPTIMIZED: Build formatted data using pre-calculated counts (no nested queries!) - const formattedData = orgRootData.map((orgRoot) => { - const rootCounts = getCounts(orgRootMap, orgRoot.id); - const rootPosCounts = getRootCounts(rootPosMap, orgRoot.id); - - return { - orgTreeId: orgRoot.id, - orgLevel: 0, - orgName: orgRoot.orgRootName, - orgTreeName: orgRoot.orgRootName, - orgTreeShortName: orgRoot.orgRootShortName, - orgTreeCode: orgRoot.orgRootCode, - orgCode: orgRoot.orgRootCode + "00", - orgTreeRank: orgRoot.orgRootRank, - orgTreeRankSub: orgRoot.orgRootRankSub, - orgRootDnaId: orgRoot.ancestorDNA, - DEPARTMENT_CODE: orgRoot.DEPARTMENT_CODE, - DIVISION_CODE: orgRoot.DIVISION_CODE, - SECTION_CODE: orgRoot.SECTION_CODE, - JOB_CODE: orgRoot.JOB_CODE, - orgTreeOrder: orgRoot.orgRootOrder, - orgTreePhoneEx: orgRoot.orgRootPhoneEx, - orgTreePhoneIn: orgRoot.orgRootPhoneIn, - orgTreeFax: orgRoot.orgRootFax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - isDeputy: orgRoot.isDeputy, - isCommission: orgRoot.isCommission, - responsibility: orgRoot.responsibility, - labelName: - orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, - totalPosition: rootCounts.totalPosition, - totalPositionCurrentUse: rootCounts.totalPositionCurrentUse, - totalPositionCurrentVacant: rootCounts.totalPositionCurrentVacant, - totalPositionNextUse: rootCounts.totalPositionNextUse, - totalPositionNextVacant: rootCounts.totalPositionNextVacant, - totalRootPosition: rootPosCounts.totalRootPosition, - totalRootPositionCurrentUse: rootPosCounts.totalRootPositionCurrentUse, - totalRootPositionCurrentVacant: rootPosCounts.totalRootPositionCurrentVacant, - totalRootPositionNextUse: rootPosCounts.totalRootPositionNextUse, - totalRootPositionNextVacant: rootPosCounts.totalRootPositionNextVacant, - children: orgChild1Data - .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) - .map((orgChild1) => { - const child1Counts = getCounts(orgChild1Map, orgChild1.id); - const child1PosKey = `${orgRoot.id}-${orgChild1.id}`; - const child1PosCounts = getRootCounts(rootPosMap, child1PosKey); - - return { - orgTreeId: orgChild1.id, - orgRootId: orgRoot.id, - orgLevel: 1, - orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild1.orgChild1Name, - orgTreeShortName: orgChild1.orgChild1ShortName, - orgTreeCode: orgChild1.orgChild1Code, - orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code, - orgTreeRank: orgChild1.orgChild1Rank, - orgTreeRankSub: orgChild1.orgChild1RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - DEPARTMENT_CODE: orgChild1.DEPARTMENT_CODE, - DIVISION_CODE: orgChild1.DIVISION_CODE, - SECTION_CODE: orgChild1.SECTION_CODE, - JOB_CODE: orgChild1.JOB_CODE, - orgTreeOrder: orgChild1.orgChild1Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild1.orgChild1PhoneEx, - orgTreePhoneIn: orgChild1.orgChild1PhoneIn, - orgTreeFax: orgChild1.orgChild1Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild1.responsibility, - isOfficer: orgChild1.isOfficer, - isInformation: orgChild1.isInformation, - labelName: - orgChild1.orgChild1Name + - " " + - orgRoot.orgRootCode + - orgChild1.orgChild1Code + - " " + - orgChild1.orgChild1ShortName, - totalPosition: child1Counts.totalPosition, - totalPositionCurrentUse: child1Counts.totalPositionCurrentUse, - totalPositionCurrentVacant: child1Counts.totalPositionCurrentVacant, - totalPositionNextUse: child1Counts.totalPositionNextUse, - totalPositionNextVacant: child1Counts.totalPositionNextVacant, - totalRootPosition: child1PosCounts.totalRootPosition, - totalRootPositionCurrentUse: child1PosCounts.totalRootPositionCurrentUse, - totalRootPositionCurrentVacant: child1PosCounts.totalRootPositionCurrentVacant, - totalRootPositionNextUse: child1PosCounts.totalRootPositionNextUse, - totalRootPositionNextVacant: child1PosCounts.totalRootPositionNextVacant, - children: orgChild2Data - .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) - .map((orgChild2) => { - const child2Counts = getCounts(orgChild2Map, orgChild2.id); - const child2PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}`; - const child2PosCounts = getRootCounts(rootPosMap, child2PosKey); - - return { - orgTreeId: orgChild2.id, - orgRootId: orgChild1.id, - orgLevel: 2, - orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild2.orgChild2Name, - orgTreeShortName: orgChild2.orgChild2ShortName, - orgTreeCode: orgChild2.orgChild2Code, - orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code, - orgTreeRank: orgChild2.orgChild2Rank, - orgTreeRankSub: orgChild2.orgChild2RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - orgChild2DnaId: orgChild2.ancestorDNA, - DEPARTMENT_CODE: orgChild2.DEPARTMENT_CODE, - DIVISION_CODE: orgChild2.DIVISION_CODE, - SECTION_CODE: orgChild2.SECTION_CODE, - JOB_CODE: orgChild2.JOB_CODE, - orgTreeOrder: orgChild2.orgChild2Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild2.orgChild2PhoneEx, - orgTreePhoneIn: orgChild2.orgChild2PhoneIn, - orgTreeFax: orgChild2.orgChild2Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild2.responsibility, - labelName: - orgChild2.orgChild2Name + - " " + - orgRoot.orgRootCode + - orgChild2.orgChild2Code + - " " + - orgChild2.orgChild2ShortName, - totalPosition: child2Counts.totalPosition, - totalPositionCurrentUse: child2Counts.totalPositionCurrentUse, - totalPositionCurrentVacant: child2Counts.totalPositionCurrentVacant, - totalPositionNextUse: child2Counts.totalPositionNextUse, - totalPositionNextVacant: child2Counts.totalPositionNextVacant, - totalRootPosition: child2PosCounts.totalRootPosition, - totalRootPositionCurrentUse: child2PosCounts.totalRootPositionCurrentUse, - totalRootPositionCurrentVacant: child2PosCounts.totalRootPositionCurrentVacant, - totalRootPositionNextUse: child2PosCounts.totalRootPositionNextUse, - totalRootPositionNextVacant: child2PosCounts.totalRootPositionNextVacant, - children: orgChild3Data - .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) - .map((orgChild3) => { - const child3Counts = getCounts(orgChild3Map, orgChild3.id); - const child3PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}`; - const child3PosCounts = getRootCounts(rootPosMap, child3PosKey); - - return { - orgTreeId: orgChild3.id, - orgRootId: orgChild2.id, - orgLevel: 3, - orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild3.orgChild3Name, - orgTreeShortName: orgChild3.orgChild3ShortName, - orgTreeCode: orgChild3.orgChild3Code, - orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code, - orgTreeRank: orgChild3.orgChild3Rank, - orgTreeRankSub: orgChild3.orgChild3RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - orgChild2DnaId: orgChild2.ancestorDNA, - orgChild3DnaId: orgChild3.ancestorDNA, - DEPARTMENT_CODE: orgChild3.DEPARTMENT_CODE, - DIVISION_CODE: orgChild3.DIVISION_CODE, - SECTION_CODE: orgChild3.SECTION_CODE, - JOB_CODE: orgChild3.JOB_CODE, - orgTreeOrder: orgChild3.orgChild3Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild3.orgChild3PhoneEx, - orgTreePhoneIn: orgChild3.orgChild3PhoneIn, - orgTreeFax: orgChild3.orgChild3Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild3.responsibility, - labelName: - orgChild3.orgChild3Name + - " " + - orgRoot.orgRootCode + - orgChild3.orgChild3Code + - " " + - orgChild3.orgChild3ShortName, - totalPosition: child3Counts.totalPosition, - totalPositionCurrentUse: child3Counts.totalPositionCurrentUse, - totalPositionCurrentVacant: child3Counts.totalPositionCurrentVacant, - totalPositionNextUse: child3Counts.totalPositionNextUse, - totalPositionNextVacant: child3Counts.totalPositionNextVacant, - totalRootPosition: child3PosCounts.totalRootPosition, - totalRootPositionCurrentUse: child3PosCounts.totalRootPositionCurrentUse, - totalRootPositionCurrentVacant: child3PosCounts.totalRootPositionCurrentVacant, - totalRootPositionNextUse: child3PosCounts.totalRootPositionNextUse, - totalRootPositionNextVacant: child3PosCounts.totalRootPositionNextVacant, - children: orgChild4Data - .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) - .map((orgChild4) => { - const child4Counts = getCounts(orgChild4Map, orgChild4.id); - const child4PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}-${orgChild4.id}`; - const child4PosCounts = getRootCounts(rootPosMap, child4PosKey); - - return { - orgTreeId: orgChild4.id, - orgRootId: orgChild3.id, - orgLevel: 4, - orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild4.orgChild4Name, - orgTreeShortName: orgChild4.orgChild4ShortName, - orgTreeCode: orgChild4.orgChild4Code, - orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code, - orgTreeRank: orgChild4.orgChild4Rank, - orgTreeRankSub: orgChild4.orgChild4RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - orgChild2DnaId: orgChild2.ancestorDNA, - orgChild3DnaId: orgChild3.ancestorDNA, - orgChild4DnaId: orgChild4.ancestorDNA, - DEPARTMENT_CODE: orgChild4.DEPARTMENT_CODE, - DIVISION_CODE: orgChild4.DIVISION_CODE, - SECTION_CODE: orgChild4.SECTION_CODE, - JOB_CODE: orgChild4.JOB_CODE, - orgTreeOrder: orgChild4.orgChild4Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild4.orgChild4PhoneEx, - orgTreePhoneIn: orgChild4.orgChild4PhoneIn, - orgTreeFax: orgChild4.orgChild4Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild4.responsibility, - labelName: - orgChild4.orgChild4Name + - " " + - orgRoot.orgRootCode + - orgChild4.orgChild4Code + - " " + - orgChild4.orgChild4ShortName, - totalPosition: child4Counts.totalPosition, - totalPositionCurrentUse: child4Counts.totalPositionCurrentUse, - totalPositionCurrentVacant: child4Counts.totalPositionCurrentVacant, - totalPositionNextUse: child4Counts.totalPositionNextUse, - totalPositionNextVacant: child4Counts.totalPositionNextVacant, - totalRootPosition: child4PosCounts.totalRootPosition, - totalRootPositionCurrentUse: child4PosCounts.totalRootPositionCurrentUse, - totalRootPositionCurrentVacant: child4PosCounts.totalRootPositionCurrentVacant, - totalRootPositionNextUse: child4PosCounts.totalRootPositionNextUse, - totalRootPositionNextVacant: child4PosCounts.totalRootPositionNextVacant, - children: [], - }; - }), - }; - }), - }; - }), - }); - }, - ); - - return new HttpSuccess(formattedData); - } diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 9730e280..db33f1ad 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -46,7 +46,7 @@ import { getRoles, addUserRoles, } from "../keycloak"; -import { getPositionCounts, getCounts, getRootCounts } from "./OrganizationController-optimized"; +// import { getPositionCounts, getCounts, getRootCounts } from "../services/OrganizationService"; import { CreatePosMasterHistoryEmployee, CreatePosMasterHistoryOfficer, @@ -1166,7 +1166,6 @@ export class OrganizationController extends Controller { async detailSuperAdmin(@Path() id: string, @Request() request: RequestWithUser) { const orgRevision = await this.orgRevisionRepository.findOne({ where: { id: id }, - relations: ["posMasters"], }); if (!orgRevision) return new HttpSuccess([]); @@ -1176,80 +1175,46 @@ export class OrganizationController extends Controller { where: { keycloak: request.user.sub, }, + select: ["id"], }); if (profile == null) return new HttpSuccess([]); - if (!request.user.role.includes("SUPER_ADMIN")) { - if (orgRevision.orgRevisionIsDraft == true && orgRevision.orgRevisionIsCurrent == false) { - rootId = - orgRevision?.posMasters?.filter((x) => x.next_holderId == profile.id)[0]?.orgRootId || - null; - if (!rootId) return new HttpSuccess([]); - } else { - rootId = - orgRevision?.posMasters?.filter((x) => x.current_holderId == profile.id)[0] - ?.orgRootId || null; - if (!rootId) return new HttpSuccess([]); - } - } + const posMaster = await this.posMasterRepository.findOne({ + where: + orgRevision.orgRevisionIsCurrent && !orgRevision.orgRevisionIsDraft + ? { + orgRevisionId: id, + current_holderId: profile.id, + } + : { + orgRevisionId: id, + next_holderId: profile.id, + }, + }); + if (!posMaster) return new HttpSuccess([]); + + rootId = posMaster.orgRootId; } + // OPTIMIZED: Get all position counts in ONE query (closed) + // const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } = + // await getPositionCounts(id); + const orgRootData = await AppDataSource.getRepository(OrgRoot) .createQueryBuilder("orgRoot") .where("orgRoot.orgRevisionId = :id", { id }) .andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", { rootId: rootId, }) - .select([ - "orgRoot.id", - "orgRoot.isDeputy", - "orgRoot.isCommission", - "orgRoot.orgRootName", - "orgRoot.orgRootShortName", - "orgRoot.orgRootCode", - "orgRoot.orgRootOrder", - "orgRoot.orgRootPhoneEx", - "orgRoot.orgRootPhoneIn", - "orgRoot.orgRootFax", - "orgRoot.orgRevisionId", - "orgRoot.orgRootRank", - "orgRoot.orgRootRankSub", - "orgRoot.DEPARTMENT_CODE", - "orgRoot.DIVISION_CODE", - "orgRoot.SECTION_CODE", - "orgRoot.JOB_CODE", - "orgRoot.responsibility", - "orgRoot.ancestorDNA", - ]) .orderBy("orgRoot.orgRootOrder", "ASC") .getMany(); + const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id) || null; const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) .createQueryBuilder("orgChild1") .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .select([ - "orgChild1.id", - "orgChild1.isOfficer", - "orgChild1.isInformation", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - "orgChild1.ancestorDNA", - ]) .orderBy("orgChild1.orgChild1Order", "ASC") .getMany() : []; @@ -1260,26 +1225,6 @@ export class OrganizationController extends Controller { ? await AppDataSource.getRepository(OrgChild2) .createQueryBuilder("orgChild2") .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .select([ - "orgChild2.id", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - "orgChild2.ancestorDNA", - ]) .orderBy("orgChild2.orgChild2Order", "ASC") .getMany() : []; @@ -1290,26 +1235,6 @@ export class OrganizationController extends Controller { ? await AppDataSource.getRepository(OrgChild3) .createQueryBuilder("orgChild3") .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .select([ - "orgChild3.id", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - "orgChild3.ancestorDNA", - ]) .orderBy("orgChild3.orgChild3Order", "ASC") .getMany() : []; @@ -1320,661 +1245,270 @@ export class OrganizationController extends Controller { ? await AppDataSource.getRepository(OrgChild4) .createQueryBuilder("orgChild4") .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .select([ - "orgChild4.id", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - "orgChild4.ancestorDNA", - ]) .orderBy("orgChild4.orgChild4Order", "ASC") .getMany() : []; - // const formattedData = orgRootData.map((orgRoot) => { - const formattedData = await Promise.all( - orgRootData.map(async (orgRoot) => { - return { - orgTreeId: orgRoot.id, - orgLevel: 0, - orgName: orgRoot.orgRootName, - orgTreeName: orgRoot.orgRootName, - orgTreeShortName: orgRoot.orgRootShortName, - orgTreeCode: orgRoot.orgRootCode, - orgCode: orgRoot.orgRootCode + "00", - orgTreeRank: orgRoot.orgRootRank, - orgTreeRankSub: orgRoot.orgRootRankSub, - orgRootDnaId: orgRoot.ancestorDNA, - DEPARTMENT_CODE: orgRoot.DEPARTMENT_CODE, - DIVISION_CODE: orgRoot.DIVISION_CODE, - SECTION_CODE: orgRoot.SECTION_CODE, - JOB_CODE: orgRoot.JOB_CODE, - orgTreeOrder: orgRoot.orgRootOrder, - orgTreePhoneEx: orgRoot.orgRootPhoneEx, - orgTreePhoneIn: orgRoot.orgRootPhoneIn, - orgTreeFax: orgRoot.orgRootFax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - isDeputy: orgRoot.isDeputy, - isCommission: orgRoot.isCommission, - responsibility: orgRoot.responsibility, - labelName: - orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, - totalPosition: await this.posMasterRepository.count({ - where: { orgRevisionId: orgRoot.orgRevisionId, orgRootId: orgRoot.id }, - }), - totalPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - current_holderId: IsNull() || "", - }, - }), - totalPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - next_holderId: IsNull() || "", - }, - }), - totalRootPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: IsNull() || "", - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - }, - }), - totalRootPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: IsNull() || "", - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: IsNull() || "", - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - current_holderId: IsNull() || "", - }, - }), - totalRootPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: IsNull() || "", - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: IsNull() || "", - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - next_holderId: IsNull() || "", - }, - }), + // OPTIMIZED: Build formatted data using pre-calculated counts (no nested queries!) + const formattedData = orgRootData.map((orgRoot) => { + // const rootCounts = getCounts(orgRootMap, orgRoot.id); + // const rootPosCounts = getRootCounts(rootPosMap, orgRoot.id); - children: await Promise.all( - orgChild1Data - .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) - .map(async (orgChild1) => ({ - orgTreeId: orgChild1.id, - orgRootId: orgRoot.id, - orgLevel: 1, - orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild1.orgChild1Name, - orgTreeShortName: orgChild1.orgChild1ShortName, - orgTreeCode: orgChild1.orgChild1Code, - orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code, - orgTreeRank: orgChild1.orgChild1Rank, - orgTreeRankSub: orgChild1.orgChild1RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - DEPARTMENT_CODE: orgChild1.DEPARTMENT_CODE, - DIVISION_CODE: orgChild1.DIVISION_CODE, - SECTION_CODE: orgChild1.SECTION_CODE, - JOB_CODE: orgChild1.JOB_CODE, - orgTreeOrder: orgChild1.orgChild1Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild1.orgChild1PhoneEx, - orgTreePhoneIn: orgChild1.orgChild1PhoneIn, - orgTreeFax: orgChild1.orgChild1Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild1.responsibility, - isOfficer: orgChild1.isOfficer, - isInformation: orgChild1.isInformation, - labelName: - orgChild1.orgChild1Name + - " " + - orgRoot.orgRootCode + - orgChild1.orgChild1Code + - " " + - orgChild1.orgChild1ShortName, - totalPosition: await this.posMasterRepository.count({ - where: { orgRevisionId: orgRoot.orgRevisionId, orgChild1Id: orgChild1.id }, - }), - totalPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild1Id: orgChild1.id, - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild1Id: orgChild1.id, - current_holderId: IsNull() || "", - }, - }), - totalPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild1Id: orgChild1.id, - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild1Id: orgChild1.id, - next_holderId: IsNull() || "", - }, - }), - totalRootPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - }, - }), - totalRootPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - current_holderId: IsNull() || "", - }, - }), - totalRootPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: IsNull() || "", - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - next_holderId: IsNull() || "", - }, - }), + return { + orgTreeId: orgRoot.id, + orgLevel: 0, + orgName: orgRoot.orgRootName, + orgTreeName: orgRoot.orgRootName, + orgTreeShortName: orgRoot.orgRootShortName, + orgTreeCode: orgRoot.orgRootCode, + orgCode: orgRoot.orgRootCode + "00", + orgTreeRank: orgRoot.orgRootRank, + orgTreeRankSub: orgRoot.orgRootRankSub, + orgRootDnaId: orgRoot.ancestorDNA, + DEPARTMENT_CODE: orgRoot.DEPARTMENT_CODE, + DIVISION_CODE: orgRoot.DIVISION_CODE, + SECTION_CODE: orgRoot.SECTION_CODE, + JOB_CODE: orgRoot.JOB_CODE, + orgTreeOrder: orgRoot.orgRootOrder, + orgTreePhoneEx: orgRoot.orgRootPhoneEx, + orgTreePhoneIn: orgRoot.orgRootPhoneIn, + orgTreeFax: orgRoot.orgRootFax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + isDeputy: orgRoot.isDeputy, + isCommission: orgRoot.isCommission, + responsibility: orgRoot.responsibility, + labelName: + orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, + // totalPosition: rootCounts.totalPosition, + // totalPositionCurrentUse: rootCounts.totalPositionCurrentUse, + // totalPositionCurrentVacant: rootCounts.totalPositionCurrentVacant, + // totalPositionNextUse: rootCounts.totalPositionNextUse, + // totalPositionNextVacant: rootCounts.totalPositionNextVacant, + // totalRootPosition: rootPosCounts.totalRootPosition, + // totalRootPositionCurrentUse: rootPosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: rootPosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: rootPosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: rootPosCounts.totalRootPositionNextVacant, + children: orgChild1Data + .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) + .map((orgChild1) => { + // const child1Counts = getCounts(orgChild1Map, orgChild1.id); + // const child1PosKey = `${orgRoot.id}-${orgChild1.id}`; + // const child1PosCounts = getRootCounts(rootPosMap, child1PosKey); - children: await Promise.all( - orgChild2Data - .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) - .map(async (orgChild2) => ({ - orgTreeId: orgChild2.id, - orgRootId: orgChild1.id, - orgLevel: 2, - orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild2.orgChild2Name, - orgTreeShortName: orgChild2.orgChild2ShortName, - orgTreeCode: orgChild2.orgChild2Code, - orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code, - orgTreeRank: orgChild2.orgChild2Rank, - orgTreeRankSub: orgChild2.orgChild2RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - orgChild2DnaId: orgChild2.ancestorDNA, - DEPARTMENT_CODE: orgChild2.DEPARTMENT_CODE, - DIVISION_CODE: orgChild2.DIVISION_CODE, - SECTION_CODE: orgChild2.SECTION_CODE, - JOB_CODE: orgChild2.JOB_CODE, - orgTreeOrder: orgChild2.orgChild2Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild2.orgChild2PhoneEx, - orgTreePhoneIn: orgChild2.orgChild2PhoneIn, - orgTreeFax: orgChild2.orgChild2Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild2.responsibility, - labelName: - orgChild2.orgChild2Name + - " " + - orgRoot.orgRootCode + - orgChild2.orgChild2Code + - " " + - orgChild2.orgChild2ShortName, - totalPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild2Id: orgChild2.id, - }, - }), - totalPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild2Id: orgChild2.id, - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), - totalPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild2Id: orgChild2.id, - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }), - totalRootPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - }, - }), - totalRootPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - current_holderId: IsNull() || "", - }, - }), - totalRootPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: IsNull() || "", - orgChild4Id: IsNull() || "", - next_holderId: IsNull() || "", - }, - }), + return { + orgTreeId: orgChild1.id, + orgRootId: orgRoot.id, + orgLevel: 1, + orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild1.orgChild1Name, + orgTreeShortName: orgChild1.orgChild1ShortName, + orgTreeCode: orgChild1.orgChild1Code, + orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code, + orgTreeRank: orgChild1.orgChild1Rank, + orgTreeRankSub: orgChild1.orgChild1RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + DEPARTMENT_CODE: orgChild1.DEPARTMENT_CODE, + DIVISION_CODE: orgChild1.DIVISION_CODE, + SECTION_CODE: orgChild1.SECTION_CODE, + JOB_CODE: orgChild1.JOB_CODE, + orgTreeOrder: orgChild1.orgChild1Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild1.orgChild1PhoneEx, + orgTreePhoneIn: orgChild1.orgChild1PhoneIn, + orgTreeFax: orgChild1.orgChild1Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild1.responsibility, + isOfficer: orgChild1.isOfficer, + isInformation: orgChild1.isInformation, + labelName: + orgChild1.orgChild1Name + + " " + + orgRoot.orgRootCode + + orgChild1.orgChild1Code + + " " + + orgChild1.orgChild1ShortName, + // totalPosition: child1Counts.totalPosition, + // totalPositionCurrentUse: child1Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child1Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child1Counts.totalPositionNextUse, + // totalPositionNextVacant: child1Counts.totalPositionNextVacant, + // totalRootPosition: child1PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: child1PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: child1PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child1PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: child1PosCounts.totalRootPositionNextVacant, + children: orgChild2Data + .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) + .map((orgChild2) => { + // const child2Counts = getCounts(orgChild2Map, orgChild2.id); + // const child2PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}`; + // const child2PosCounts = getRootCounts(rootPosMap, child2PosKey); - children: await Promise.all( - orgChild3Data - .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) - .map(async (orgChild3) => ({ - orgTreeId: orgChild3.id, - orgRootId: orgChild2.id, - orgLevel: 3, - orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild3.orgChild3Name, - orgTreeShortName: orgChild3.orgChild3ShortName, - orgTreeCode: orgChild3.orgChild3Code, - orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code, - orgTreeRank: orgChild3.orgChild3Rank, - orgTreeRankSub: orgChild3.orgChild3RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - orgChild2DnaId: orgChild2.ancestorDNA, - orgChild3DnaId: orgChild3.ancestorDNA, - DEPARTMENT_CODE: orgChild3.DEPARTMENT_CODE, - DIVISION_CODE: orgChild3.DIVISION_CODE, - SECTION_CODE: orgChild3.SECTION_CODE, - JOB_CODE: orgChild3.JOB_CODE, - orgTreeOrder: orgChild3.orgChild3Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild3.orgChild3PhoneEx, - orgTreePhoneIn: orgChild3.orgChild3PhoneIn, - orgTreeFax: orgChild3.orgChild3Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild3.responsibility, - labelName: - orgChild3.orgChild3Name + - " " + - orgRoot.orgRootCode + - orgChild3.orgChild3Code + - " " + - orgChild3.orgChild3ShortName, - totalPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild3Id: orgChild3.id, - }, - }), - totalPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild3Id: orgChild3.id, - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), - totalPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild3Id: orgChild3.id, - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }), - totalRootPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: IsNull() || "", - }, - }), - totalRootPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: IsNull() || "", - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: IsNull() || "", - current_holderId: IsNull() || "", - }, - }), - totalRootPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: IsNull() || "", - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: IsNull() || "", - next_holderId: IsNull() || "", - }, - }), + return { + orgTreeId: orgChild2.id, + orgRootId: orgChild1.id, + orgLevel: 2, + orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild2.orgChild2Name, + orgTreeShortName: orgChild2.orgChild2ShortName, + orgTreeCode: orgChild2.orgChild2Code, + orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code, + orgTreeRank: orgChild2.orgChild2Rank, + orgTreeRankSub: orgChild2.orgChild2RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + DEPARTMENT_CODE: orgChild2.DEPARTMENT_CODE, + DIVISION_CODE: orgChild2.DIVISION_CODE, + SECTION_CODE: orgChild2.SECTION_CODE, + JOB_CODE: orgChild2.JOB_CODE, + orgTreeOrder: orgChild2.orgChild2Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild2.orgChild2PhoneEx, + orgTreePhoneIn: orgChild2.orgChild2PhoneIn, + orgTreeFax: orgChild2.orgChild2Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild2.responsibility, + labelName: + orgChild2.orgChild2Name + + " " + + orgRoot.orgRootCode + + orgChild2.orgChild2Code + + " " + + orgChild2.orgChild2ShortName, + // totalPosition: child2Counts.totalPosition, + // totalPositionCurrentUse: child2Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child2Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child2Counts.totalPositionNextUse, + // totalPositionNextVacant: child2Counts.totalPositionNextVacant, + // totalRootPosition: child2PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: child2PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: child2PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child2PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: child2PosCounts.totalRootPositionNextVacant, + children: orgChild3Data + .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) + .map((orgChild3) => { + // const child3Counts = getCounts(orgChild3Map, orgChild3.id); + // const child3PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}`; + // const child3PosCounts = getRootCounts(rootPosMap, child3PosKey); - children: await Promise.all( - orgChild4Data - .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) - .map(async (orgChild4) => ({ - orgTreeId: orgChild4.id, - orgRootId: orgChild3.id, - orgLevel: 4, - orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild4.orgChild4Name, - orgTreeShortName: orgChild4.orgChild4ShortName, - orgTreeCode: orgChild4.orgChild4Code, - orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code, - orgTreeRank: orgChild4.orgChild4Rank, - orgTreeRankSub: orgChild4.orgChild4RankSub, - orgRootDnaId: orgRoot.ancestorDNA, - orgChild1DnaId: orgChild1.ancestorDNA, - orgChild2DnaId: orgChild2.ancestorDNA, - orgChild3DnaId: orgChild3.ancestorDNA, - orgChild4DnaId: orgChild4.ancestorDNA, - DEPARTMENT_CODE: orgChild4.DEPARTMENT_CODE, - DIVISION_CODE: orgChild4.DIVISION_CODE, - SECTION_CODE: orgChild4.SECTION_CODE, - JOB_CODE: orgChild4.JOB_CODE, - orgTreeOrder: orgChild4.orgChild4Order, - orgRootCode: orgRoot.orgRootCode, - orgTreePhoneEx: orgChild4.orgChild4PhoneEx, - orgTreePhoneIn: orgChild4.orgChild4PhoneIn, - orgTreeFax: orgChild4.orgChild4Fax, - orgRevisionId: orgRoot.orgRevisionId, - orgRootName: orgRoot.orgRootName, - responsibility: orgChild4.responsibility, - labelName: - orgChild4.orgChild4Name + - " " + - orgRoot.orgRootCode + - orgChild4.orgChild4Code + - " " + - orgChild4.orgChild4ShortName, - totalPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild4Id: orgChild4.id, - }, - }), - totalPositionCurrentUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild4Id: orgChild4.id, - current_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionCurrentVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), - totalPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild4Id: orgChild4.id, - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalPositionNextVacant: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }), - totalRootPosition: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - }, - }), - totalRootPositionCurrentUse: await this.posMasterRepository.count( - { - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: Not(IsNull()) || Not(""), - }, - }, - ), - totalRootPositionCurrentVacant: - await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), - totalRootPositionNextUse: await this.posMasterRepository.count({ - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: Not(IsNull()) || Not(""), - }, - }), - totalRootPositionNextVacant: await this.posMasterRepository.count( - { - where: { - orgRevisionId: orgRoot.orgRevisionId, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }, - ), - })), - ), - })), - ), - })), - ), - })), - ), - }; - }), - ); + return { + orgTreeId: orgChild3.id, + orgRootId: orgChild2.id, + orgLevel: 3, + orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild3.orgChild3Name, + orgTreeShortName: orgChild3.orgChild3ShortName, + orgTreeCode: orgChild3.orgChild3Code, + orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code, + orgTreeRank: orgChild3.orgChild3Rank, + orgTreeRankSub: orgChild3.orgChild3RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + orgChild3DnaId: orgChild3.ancestorDNA, + DEPARTMENT_CODE: orgChild3.DEPARTMENT_CODE, + DIVISION_CODE: orgChild3.DIVISION_CODE, + SECTION_CODE: orgChild3.SECTION_CODE, + JOB_CODE: orgChild3.JOB_CODE, + orgTreeOrder: orgChild3.orgChild3Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild3.orgChild3PhoneEx, + orgTreePhoneIn: orgChild3.orgChild3PhoneIn, + orgTreeFax: orgChild3.orgChild3Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild3.responsibility, + labelName: + orgChild3.orgChild3Name + + " " + + orgRoot.orgRootCode + + orgChild3.orgChild3Code + + " " + + orgChild3.orgChild3ShortName, + // totalPosition: child3Counts.totalPosition, + // totalPositionCurrentUse: child3Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child3Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child3Counts.totalPositionNextUse, + // totalPositionNextVacant: child3Counts.totalPositionNextVacant, + // totalRootPosition: child3PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: child3PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: + // child3PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child3PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: child3PosCounts.totalRootPositionNextVacant, + children: orgChild4Data + .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) + .map((orgChild4) => { + // const child4Counts = getCounts(orgChild4Map, orgChild4.id); + // const child4PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}-${orgChild4.id}`; + // const child4PosCounts = getRootCounts(rootPosMap, child4PosKey); + + return { + orgTreeId: orgChild4.id, + orgRootId: orgChild3.id, + orgLevel: 4, + orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild4.orgChild4Name, + orgTreeShortName: orgChild4.orgChild4ShortName, + orgTreeCode: orgChild4.orgChild4Code, + orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code, + orgTreeRank: orgChild4.orgChild4Rank, + orgTreeRankSub: orgChild4.orgChild4RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + orgChild3DnaId: orgChild3.ancestorDNA, + orgChild4DnaId: orgChild4.ancestorDNA, + DEPARTMENT_CODE: orgChild4.DEPARTMENT_CODE, + DIVISION_CODE: orgChild4.DIVISION_CODE, + SECTION_CODE: orgChild4.SECTION_CODE, + JOB_CODE: orgChild4.JOB_CODE, + orgTreeOrder: orgChild4.orgChild4Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild4.orgChild4PhoneEx, + orgTreePhoneIn: orgChild4.orgChild4PhoneIn, + orgTreeFax: orgChild4.orgChild4Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild4.responsibility, + labelName: + orgChild4.orgChild4Name + + " " + + orgRoot.orgRootCode + + orgChild4.orgChild4Code + + " " + + orgChild4.orgChild4ShortName, + // totalPosition: child4Counts.totalPosition, + // totalPositionCurrentUse: child4Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child4Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child4Counts.totalPositionNextUse, + // totalPositionNextVacant: child4Counts.totalPositionNextVacant, + // totalRootPosition: child4PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: + // child4PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: + // child4PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child4PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: + // child4PosCounts.totalRootPositionNextVacant, + children: [], + }; + }), + }; + }), + }; + }), + }; + }), + }; + }); return new HttpSuccess(formattedData); } diff --git a/src/controllers/OrganizationController-optimized.ts b/src/services/OrganizationService.ts similarity index 92% rename from src/controllers/OrganizationController-optimized.ts rename to src/services/OrganizationService.ts index a5de09b6..a9b2b796 100644 --- a/src/controllers/OrganizationController-optimized.ts +++ b/src/services/OrganizationService.ts @@ -12,6 +12,8 @@ export async function getPositionCounts(orgRevisionId: string) { "pos.orgChild2Id", "pos.orgChild3Id", "pos.orgChild4Id", + "pos.current_holderId", + "pos.next_holderId", ]) .where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId }) .getMany(); @@ -27,7 +29,7 @@ export async function getPositionCounts(orgRevisionId: string) { const orgChild4Map = new Map(); // Nested maps for root positions (positions at specific levels) - const rootPosMap = new Map(); // key: "rootId", "rootId-child1Id", "rootId-child1Id-child2Id", etc. + const rootPosMap = new Map(); for (const pos of rawData) { const orgRootId = pos.orgRootId || "NULL"; @@ -57,8 +59,8 @@ export async function getPositionCounts(orgRevisionId: string) { // Level 1 (orgChild1) counts if (!isNull(pos.orgChild1Id)) { - if (!orgChild1Map.has(pos.orgChild1Id)) { - orgChild1Map.set(pos.orgChild1Id, { + if (!orgChild1Map.has(orgChild1Id)) { + orgChild1Map.set(orgChild1Id, { totalPosition: 0, totalPositionCurrentUse: 0, totalPositionCurrentVacant: 0, @@ -66,7 +68,7 @@ export async function getPositionCounts(orgRevisionId: string) { totalPositionNextVacant: 0, }); } - const child1Counts = orgChild1Map.get(pos.orgChild1Id); + const child1Counts = orgChild1Map.get(orgChild1Id); child1Counts.totalPosition++; if (!isNull(pos.current_holderId)) child1Counts.totalPositionCurrentUse++; else child1Counts.totalPositionCurrentVacant++; @@ -76,8 +78,8 @@ export async function getPositionCounts(orgRevisionId: string) { // Level 2 (orgChild2) counts if (!isNull(pos.orgChild2Id)) { - if (!orgChild2Map.has(pos.orgChild2Id)) { - orgChild2Map.set(pos.orgChild2Id, { + if (!orgChild2Map.has(orgChild2Id)) { + orgChild2Map.set(orgChild2Id, { totalPosition: 0, totalPositionCurrentUse: 0, totalPositionCurrentVacant: 0, @@ -85,7 +87,7 @@ export async function getPositionCounts(orgRevisionId: string) { totalPositionNextVacant: 0, }); } - const child2Counts = orgChild2Map.get(pos.orgChild2Id); + const child2Counts = orgChild2Map.get(orgChild2Id); child2Counts.totalPosition++; if (!isNull(pos.current_holderId)) child2Counts.totalPositionCurrentUse++; else child2Counts.totalPositionCurrentVacant++; @@ -95,8 +97,8 @@ export async function getPositionCounts(orgRevisionId: string) { // Level 3 (orgChild3) counts if (!isNull(pos.orgChild3Id)) { - if (!orgChild3Map.has(pos.orgChild3Id)) { - orgChild3Map.set(pos.orgChild3Id, { + if (!orgChild3Map.has(orgChild3Id)) { + orgChild3Map.set(orgChild3Id, { totalPosition: 0, totalPositionCurrentUse: 0, totalPositionCurrentVacant: 0, @@ -104,7 +106,7 @@ export async function getPositionCounts(orgRevisionId: string) { totalPositionNextVacant: 0, }); } - const child3Counts = orgChild3Map.get(pos.orgChild3Id); + const child3Counts = orgChild3Map.get(orgChild3Id); child3Counts.totalPosition++; if (!isNull(pos.current_holderId)) child3Counts.totalPositionCurrentUse++; else child3Counts.totalPositionCurrentVacant++; @@ -114,8 +116,8 @@ export async function getPositionCounts(orgRevisionId: string) { // Level 4 (orgChild4) counts if (!isNull(pos.orgChild4Id)) { - if (!orgChild4Map.has(pos.orgChild4Id)) { - orgChild4Map.set(pos.orgChild4Id, { + if (!orgChild4Map.has(orgChild4Id)) { + orgChild4Map.set(orgChild4Id, { totalPosition: 0, totalPositionCurrentUse: 0, totalPositionCurrentVacant: 0, @@ -123,7 +125,7 @@ export async function getPositionCounts(orgRevisionId: string) { totalPositionNextVacant: 0, }); } - const child4Counts = orgChild4Map.get(pos.orgChild4Id); + const child4Counts = orgChild4Map.get(orgChild4Id); child4Counts.totalPosition++; if (!isNull(pos.current_holderId)) child4Counts.totalPositionCurrentUse++; else child4Counts.totalPositionCurrentVacant++; @@ -198,7 +200,7 @@ export async function getPositionCounts(orgRevisionId: string) { // For orgChild3 level if (!isNull(pos.orgChild3Id)) { - const child3LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${pos.orgChild3Id}`; + const child3LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}`; if (!rootPosMap.has(child3LevelKey)) { rootPosMap.set(child3LevelKey, { totalRootPosition: 0, @@ -220,7 +222,7 @@ export async function getPositionCounts(orgRevisionId: string) { // For orgChild4 level if (!isNull(pos.orgChild4Id)) { - const child4LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${pos.orgChild3Id}-${pos.orgChild4Id}`; + const child4LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}-${pos.orgChild4Id}`; if (!rootPosMap.has(child4LevelKey)) { rootPosMap.set(child4LevelKey, { totalRootPosition: 0, From 7c70229579fbcf5b43bd9ded9c1f8925c8019305 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 28 Jan 2026 17:48:28 +0700 Subject: [PATCH 007/178] fix: query use Promise all --- src/controllers/OrganizationController.ts | 65 ++++++++++++++--------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index db33f1ad..8b027cdb 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -1200,6 +1200,7 @@ export class OrganizationController extends Controller { // const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } = // await getPositionCounts(id); + // OPTIMIZED: Fetch orgRoot first, then fetch all child levels in parallel const orgRootData = await AppDataSource.getRepository(OrgRoot) .createQueryBuilder("orgRoot") .where("orgRoot.orgRevisionId = :id", { id }) @@ -1209,44 +1210,60 @@ export class OrganizationController extends Controller { .orderBy("orgRoot.orgRootOrder", "ASC") .getMany(); - const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id) || null; + const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id); + + // OPTIMIZED: Fetch all child levels in parallel using orgRevisionId + // This is faster than sequential queries that depend on parent IDs + const [orgChild1AllData, orgChild2AllData, orgChild3AllData, orgChild4AllData] = + await Promise.all([ + AppDataSource.getRepository(OrgChild1) + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRevisionId = :id", { id }) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany(), + + AppDataSource.getRepository(OrgChild2) + .createQueryBuilder("orgChild2") + .where("orgChild2.orgRevisionId = :id", { id }) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany(), + + AppDataSource.getRepository(OrgChild3) + .createQueryBuilder("orgChild3") + .where("orgChild3.orgRevisionId = :id", { id }) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany(), + + AppDataSource.getRepository(OrgChild4) + .createQueryBuilder("orgChild4") + .where("orgChild4.orgRevisionId = :id", { id }) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany(), + ]); + + // Filter child1 data by orgRootIds (maintains backward compatibility) const orgChild1Data = orgRootIds && orgRootIds.length > 0 - ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + ? orgChild1AllData.filter((orgChild1) => orgRootIds.includes(orgChild1.orgRootId)) : []; - const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; + // Build maps for efficient filtering of deeper levels + const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id); const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 - ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + ? orgChild2AllData.filter((orgChild2) => orgChild1Ids.includes(orgChild2.orgChild1Id)) : []; - const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; + const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id); const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 - ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + ? orgChild3AllData.filter((orgChild3) => orgChild2Ids.includes(orgChild3.orgChild2Id)) : []; - const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; + const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id); const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 - ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + ? orgChild4AllData.filter((orgChild4) => orgChild3Ids.includes(orgChild4.orgChild3Id)) : []; // OPTIMIZED: Build formatted data using pre-calculated counts (no nested queries!) From 1a324af4833e36b70ba3b0834f06ff7493793315 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 28 Jan 2026 18:26:03 +0700 Subject: [PATCH 008/178] fix: api /super-admin/{id} memory cache --- src/app.ts | 6 +- src/controllers/OrganizationController.ts | 10 ++ src/middlewares/logs.ts | 2 +- ...{log-memory-store.ts => LogMemoryStore.ts} | 0 src/utils/OrgStructureCache.ts | 96 +++++++++++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) rename src/utils/{log-memory-store.ts => LogMemoryStore.ts} (100%) create mode 100644 src/utils/OrgStructureCache.ts diff --git a/src/app.ts b/src/app.ts index 0225f162..a44cace5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,8 @@ import { AppDataSource } from "./database/data-source"; import { RegisterRoutes } from "./routes"; import { OrganizationController } from "./controllers/OrganizationController"; import logMiddleware from "./middlewares/logs"; -import { logMemoryStore } from "./utils/log-memory-store"; +import { logMemoryStore } from "./utils/LogMemoryStore"; +import { orgStructureCache } from "./utils/OrgStructureCache"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; import { DateSerializer } from "./interfaces/date-serializer"; @@ -24,6 +25,9 @@ async function main() { // Initialize LogMemoryStore after database is ready logMemoryStore.initialize(); + // Initialize OrgStructureCache after database is ready + orgStructureCache.initialize(); + // Setup custom Date serialization for local timezone DateSerializer.setupDateSerialization(); diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8b027cdb..ad1f71d5 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -51,6 +51,7 @@ import { CreatePosMasterHistoryEmployee, CreatePosMasterHistoryOfficer, } from "../services/PositionService"; +import { orgStructureCache } from "../utils/OrgStructureCache"; @Route("api/v1/org") @Tags("Organization") @@ -1196,6 +1197,12 @@ export class OrganizationController extends Controller { rootId = posMaster.orgRootId; } + // OPTIMIZED: Check cache first + const cachedResponse = await orgStructureCache.get(id, rootId); + if (cachedResponse) { + return new HttpSuccess(cachedResponse); + } + // OPTIMIZED: Get all position counts in ONE query (closed) // const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } = // await getPositionCounts(id); @@ -1527,6 +1534,9 @@ export class OrganizationController extends Controller { }; }); + // OPTIMIZED: Cache the result + await orgStructureCache.set(id, rootId, formattedData); + return new HttpSuccess(formattedData); } diff --git a/src/middlewares/logs.ts b/src/middlewares/logs.ts index 99e7e31a..1bff2060 100644 --- a/src/middlewares/logs.ts +++ b/src/middlewares/logs.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from "express"; import { Client } from "@elastic/elasticsearch"; -import { logMemoryStore } from "../utils/log-memory-store"; +import { logMemoryStore } from "../utils/LogMemoryStore"; if (!process.env.ELASTICSEARCH_INDEX) { throw new Error("Require ELASTICSEARCH_INDEX to store log."); diff --git a/src/utils/log-memory-store.ts b/src/utils/LogMemoryStore.ts similarity index 100% rename from src/utils/log-memory-store.ts rename to src/utils/LogMemoryStore.ts diff --git a/src/utils/OrgStructureCache.ts b/src/utils/OrgStructureCache.ts new file mode 100644 index 00000000..33fa19e2 --- /dev/null +++ b/src/utils/OrgStructureCache.ts @@ -0,0 +1,96 @@ +interface CacheEntry { + data: any; + cachedAt: Date; +} + +class OrgStructureCache { + private cache: Map = new Map(); + private readonly CACHE_TTL = 10 * 60 * 1000; // 10 minutes + private isInitialized = false; + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor() { + // Don't auto-initialize - wait for AppDataSource to be ready + } + + initialize() { + if (this.isInitialized) return; + + this.isInitialized = true; + // Cleanup expired entries every 10 minutes + this.cleanupTimer = setInterval(() => { + this.cleanup(); + }, this.CACHE_TTL); + + console.log("[OrgStructureCache] Initialized"); + } + + private generateKey(revisionId: string, rootId?: string): string { + return `org-structure-${revisionId}-${rootId || "all"}`; + } + + private cleanup() { + const now = Date.now(); + let cleaned = 0; + + for (const [key, entry] of this.cache.entries()) { + const age = now - entry.cachedAt.getTime(); + if (age > this.CACHE_TTL) { + this.cache.delete(key); + cleaned++; + } + } + + if (cleaned > 0) { + console.log(`[OrgStructureCache] Cleaned ${cleaned} expired entries`); + } + } + + async get(revisionId: string, rootId?: string): Promise { + const key = this.generateKey(revisionId, rootId); + const entry = this.cache.get(key); + + if (!entry) { + return null; + } + + // Check if expired + const age = Date.now() - entry.cachedAt.getTime(); + if (age > this.CACHE_TTL) { + this.cache.delete(key); + return null; + } + + console.log(`[OrgStructureCache] HIT: ${key}`); + return entry.data; + } + + async set(revisionId: string, rootId: string | undefined, data: any): Promise { + const key = this.generateKey(revisionId, rootId); + this.cache.set(key, { + data, + cachedAt: new Date(), + }); + console.log(`[OrgStructureCache] SET: ${key}`); + } + + invalidate(revisionId: string): void { + // Invalidate all entries for this revision + for (const key of this.cache.keys()) { + if (key.startsWith(`org-structure-${revisionId}`)) { + this.cache.delete(key); + } + } + console.log(`[OrgStructureCache] INVALIDATED: ${revisionId}`); + } + + destroy() { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + this.cache.clear(); + } +} + +export const orgStructureCache = new OrgStructureCache(); From e4cfac2eb2c3ca2073d74c94c27f19120d5071d1 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 28 Jan 2026 23:10:50 +0700 Subject: [PATCH 009/178] add: docs and backup file --- docs/SUMMARY_OPTIMIZATION-fix-optimization.md | 281 +++++++++++++ src/controllers/super-admin-{id}-bak.txt | 368 ++++++++++++++++++ 2 files changed, 649 insertions(+) create mode 100644 docs/SUMMARY_OPTIMIZATION-fix-optimization.md create mode 100644 src/controllers/super-admin-{id}-bak.txt diff --git a/docs/SUMMARY_OPTIMIZATION-fix-optimization.md b/docs/SUMMARY_OPTIMIZATION-fix-optimization.md new file mode 100644 index 00000000..01dcb2a6 --- /dev/null +++ b/docs/SUMMARY_OPTIMIZATION-fix-optimization.md @@ -0,0 +1,281 @@ +# สรุปการปรับปรุง +## Branch: `fix/optimization-detailSuperAdmin` + +--- + +## 📋 ภาพรวม + +การแก้ไขครั้งนี้มุ่งเน้นปรับปรุงประสิทธิภาพและความมั่นคงของ API `GET /super-admin/{id}` ซึ่งมีปัญหาเรื่อง: +- Query ฐานข้อมูลซ้ำซ้อนหลายครั้ง +- การใช้งาน database connection ไม่มีประสิทธิภาพ +- ขาดระบบ caching ที่เหมาะสม +- ขาดระบบ Graceful Shutdown + +--- + +## 🔧 รายละเอียดการแก้ไขแต่ละส่วน + +### 1. Connection Pool Settings (`data-source.ts`) + +**ไฟล์:** `src/database/data-source.ts` + +**การแก้ไข:** +```typescript +// เพิ่ม connection pool settings +extra: { + connectionLimit: +(process.env.DB_CONNECTION_LIMIT || 50), + maxIdle: +(process.env.DB_MAX_IDLE || 10), + idleTimeout: +(process.env.DB_IDLE_TIMEOUT || 60000), + timezone: "+07:00", +}, +poolSize: +(process.env.DB_POOL_SIZE || 10), +maxQueryExecutionTime: +(process.env.DB_MAX_QUERY_TIME || 3000), +``` + +**คำอธิบายเชิงเทคนิค:** +- `connectionLimit: 50` - จำกัดจำนวน connection สูงสุดที่เปิดพร้อมกัน +- `maxIdle: 10` - จำนวน idle connection ที่เก็บไว้ reuse +- `idleTimeout: 60000` - เวลา (ms) ที่ idle connection จะถูกปิดอัตโนมัติ +- `poolSize: 10` - ขนาด connection pool ของ TypeORM +- `maxQueryExecutionTime: 3000` - แจ้งเตือนเมื่อ query ช้ากว่า 3 วินาที + +**ประโยชน์:** ป้องกัน connection exhaustion และปรับปรุงการใช้งานทรัพยากรฐานข้อมูล + +--- + +### 2. Graceful Shutdown (`app.ts`) + +**ไฟล์:** `src/app.ts` (บรรทัด 123-162) + +**การแก้ไข:** +```typescript +const gracefulShutdown = async (signal: string) => { + console.log(`\n[APP] ${signal} received. Starting graceful shutdown...`); + + // 1. หยุดรับ connection ใหม่ + server.close(() => { + console.log("[APP] HTTP server closed"); + }); + + // 2. ปิด database connections + if (AppDataSource.isInitialized) { + await AppDataSource.destroy(); + console.log("[APP] Database connections closed"); + } + + // 3. ทำลาย cache instances + logMemoryStore.destroy(); + orgStructureCache.destroy(); + + // 4. บังคับปิดหลังจาก 30 วินาที (หาก shutdown ค้าง) + const shutdownTimeout = setTimeout(() => { + process.exit(1); + }, 30000); +}; + +// ดักจับ signals +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); +process.on("SIGINT", () => gracefulShutdown("SIGINT")); +``` + +**คำอธิบายเชิงเทคนิค:** +- `SIGTERM` - signal ที่ระบบส่งมาเมื่อต้องการ stop service +- `SIGINT` - signal จากการกด Ctrl+C +- ปิดทีละขั้นตอน: HTTP Server → Database → Cache +- Timeout 30 วินาทีป้องกันการ hang ถ้า shutdown ไม่สำเร็จ + +**ประโยชน์:** ป้องกัน connection หลุดและ data loss เมื่อระบบ restart + +--- + +### 3. Log Middleware & Memory Store + +#### 3.1 Log Memory Store (`src/utils/LogMemoryStore.ts`) + +**คุณสมบัติ:** +```typescript +class LogMemoryStore { + private cache: { + currentRevision: OrgRevision | null, + profileCache: Map, // keycloak → Profile + rootIdCache: Map, // profileId → rootId + }; + private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 นาที +} +``` + +**การทำงาน:** +- Cache `currentRevision` ทุก 10 นาที +- Lazy load `profileCache` และ `rootIdCache` (โหลดเมื่อถูกเรียกใช้) +- Method `getProfileByKeycloak()` - ดึง profile จาก cache หรือ database +- Method `getRootIdByProfileId()` - ดึง rootId จาก cache หรือ database + +#### 3.2 Log Middleware (`src/middlewares/logs.ts`) + +**การเปลี่ยนแปลงหลัก:** +```typescript +// ก่อน: Query ทุกครั้งที่มี request +const profile = await AppDataSource.getRepository(Profile) + .findOne({ where: { keycloak } }); + +// หลัง: ใช้ cache +const profile = await logMemoryStore.getProfileByKeycloak(keycloak); +``` + +**ประโยชน์:** ลดจำนวน query สำหรับ log middleware อย่างมาก + +--- + +### 4. OrgStructureCache (`src/utils/OrgStructureCache.ts`) + +**ไฟล์ใหม่:** `src/utils/OrgStructureCache.ts` + +**คุณสมบัติ:** +```typescript +class OrgStructureCache { + private cache: Map; + private readonly CACHE_TTL = 10 * 60 * 1000; // 10 นาที + + // Key format: org-structure-{revisionId}-{rootId} + private generateKey(revisionId: string, rootId?: string): string + + async get(revisionId: string, rootId?: string): Promise + async set(revisionId: string, rootId: string, data: any): Promise + invalidate(revisionId: string): void +} +``` + +**การทำงาน:** +- Cache ผลลัพธ์ของ org structure ตาม `revisionId` และ `rootId` +- TTL 10 นาที - ข้อมูลเก่าจะถูกลบอัตโนมัติ +- Method `invalidate()` - ลบ cache เมื่อมีการอัปเดต revision +- Auto cleanup ทุก 10 นาที + +**การใช้งานใน API:** +```typescript +// OrganizationController.ts - detailSuperAdmin() +const cached = await orgStructureCache.get(revisionId, rootId); +if (cached) return cached; + +// ... query และคำนวณข้อมูล ... +await orgStructureCache.set(revisionId, rootId, result); +``` + +--- + +### 5. API Optimization - Promise.all + +**ไฟล์:** `src/controllers/OrganizationController.ts` + +**ก่อนแก้ไข:** +```typescript +// Query ทีละตัว - sequential +const rootOrg = await this.orgRootRepository.findOne(...); +const position = await this.posMasterRepository.findOne(...); +const ancestors = await this.orgRootRepository.find(...); +// ... อีกหลาย query ... +``` + +**หลังแก้ไข:** +```typescript +// Query พร้อมกัน - parallel +const [rootOrg, position, ancestors, ...] = await Promise.all([ + this.orgRootRepository.findOne(...), + this.posMasterRepository.findOne(...), + this.orgRootRepository.find(...), + // ... อีกหลาย query ... +]); +``` + +**คำอธิบายเชิงเทคนิค:** +- `Promise.all()` ทำให้ query ที่ไม่ depended กันรัน parallel +- ลดเวลา total จาก `t1 + t2 + t3 + ...` เหลือ `max(t1, t2, t3, ...)` +- ตัวอย่าง: ถ้ามี 10 query ใช้เวลา 100ms แต่ละตัว + - Sequential: 10 × 100ms = 1,000ms + - Parallel: ~100ms (เร็วขึ้น 10 เท่า) + +**ประโยชน์:** ลด response time อย่างมาก + +--- + +### 6. OrganizationService Refactoring (ตอนนี้ไม่ได้ใช้เพราะตัด total position counts ออก) + +**ไฟล์:** `src/services/OrganizationService.ts` + +**ฟังก์ชัน `getPositionCounts()`:** +- Query ข้อมูล position ทั้งหมดใน revision ครั้งเดียว +- สร้าง Map สำหรับ aggregate counts แต่ละระดับ (orgRoot, orgChild1-4) +- Return ผลลัพธ์เป็น Map structure ที่พร้อมใช้งาน + +**ประโยชน์:** ลดจำนวน query จากหลายร้อยครั้งเหลือ 1 ครั้ง + +--- + +## 📊 สรุปการเปลี่ยนแปลง + +| ไฟล์ | การเปลี่ยนแปลง | ผลกระทบ | +|------|------------------|---------| +| `src/database/data-source.ts` | +12 บรรทัด | Connection Pool Settings | +| `src/app.ts` | +40 บรรทัด | Graceful Shutdown | +| `src/middlewares/logs.ts` | +2 บรรทัด | Use Memory Cache | +| `src/utils/LogMemoryStore.ts` | New File | Profile/RootId Cache | +| `src/utils/OrgStructureCache.ts` | New File | Org Structure Cache | +| `src/controllers/OrganizationController.ts` | -1006 บรรทัด | Refactor + Promise.all | +| `src/services/OrganizationService.ts` | New File (Not Used) | getPositionCounts Helper | + +--- + +## 🎯 ผลลัพธ์ + +### ประสิทธิภาพ +- ⚡ Response time ลดลงอย่างมีนัยสำคัญจากการใช้ `Promise.all` +- 💾 จำนวน database query ลดลง 80-90% +- 🔄 Cache hit rate เพิ่มขึ้นสำหรับ request ซ้ำ + +### ความมั่นคง +- 🛡️ ป้องกัน connection exhaustion ด้วย connection pool +- 🔌 Graceful shutdown ป้องกัน data loss +- 📝 Log tracking ดีขึ้นด้วย memory store + +### Code Quality +- 🧹 Code ลดลง >1,000 บรรทัดจากการ refactoring +- 📦 ฟังก์ชันแยกเป็น module ที่ชัดเจน +- 🔧 ง่ายต่อการ maintain และ test + +--- + +## 🚀 วิธีการ Deploy + +1. **ตรวจสอบ Environment Variables:** + ```bash + DB_CONNECTION_LIMIT=50 + DB_MAX_IDLE=10 + DB_IDLE_TIMEOUT=60000 + DB_POOL_SIZE=10 + DB_MAX_QUERY_TIME=3000 + ``` + +2. **ตรวจสอบ Logs:** + ``` + [LogMemoryStore] Initialized with 600 second refresh interval + [OrgStructureCache] Initialized + [APP] Application is running on: http://localhost:3000 + ``` + +--- + +## 📝 Commits ที่เกี่ยวข้อง + +``` +1a324af4 fix: api /super-admin/{id} memory cache +7c702295 fix: query use Promise all +5dcb5963 fix: Api GET /super-admin/{id} +e068aafe fix: เพิ่ม Graceful Shutdown - ป้องกัน connection in app file, Log Mnddleware + Memory Store +a194d859 fix: connection pool settings +``` + +--- + +**วันที่สร้างเอกสาร:** 28 มกราคม 2026 +**Branch:** fix/optimization-detailSuperAdmin +**ผู้ดำเนินการ:** Warunee.T diff --git a/src/controllers/super-admin-{id}-bak.txt b/src/controllers/super-admin-{id}-bak.txt new file mode 100644 index 00000000..72475c82 --- /dev/null +++ b/src/controllers/super-admin-{id}-bak.txt @@ -0,0 +1,368 @@ + /** + * API รายละเอียดโครงสร้าง + * + * @summary ORG_023 - รายละเอียดโครงสร้าง (ADMIN) #25 + * + */ + @Get("super-admin/{id}") + async detailSuperAdmin(@Path() id: string, @Request() request: RequestWithUser) { + const orgRevision = await this.orgRevisionRepository.findOne({ + where: { id: id }, + }); + if (!orgRevision) return new HttpSuccess([]); + + let rootId: any = null; + if (!request.user.role.includes("SUPER_ADMIN")) { + const profile = await this.profileRepo.findOne({ + where: { + keycloak: request.user.sub, + }, + select: ["id"], + }); + if (profile == null) return new HttpSuccess([]); + + const posMaster = await this.posMasterRepository.findOne({ + where: + orgRevision.orgRevisionIsCurrent && !orgRevision.orgRevisionIsDraft + ? { + orgRevisionId: id, + current_holderId: profile.id, + } + : { + orgRevisionId: id, + next_holderId: profile.id, + }, + }); + if (!posMaster) return new HttpSuccess([]); + + rootId = posMaster.orgRootId; + } + + // OPTIMIZED: Get all position counts in ONE query (closed) + // const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } = + // await getPositionCounts(id); + + // OPTIMIZED: Fetch orgRoot first, then fetch all child levels in parallel + const orgRootData = await AppDataSource.getRepository(OrgRoot) + .createQueryBuilder("orgRoot") + .where("orgRoot.orgRevisionId = :id", { id }) + .andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", { + rootId: rootId, + }) + .orderBy("orgRoot.orgRootOrder", "ASC") + .getMany(); + + const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id); + + // OPTIMIZED: Fetch all child levels in parallel using orgRevisionId + // This is faster than sequential queries that depend on parent IDs + const [orgChild1AllData, orgChild2AllData, orgChild3AllData, orgChild4AllData] = await Promise.all([ + AppDataSource.getRepository(OrgChild1) + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRevisionId = :id", { id }) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany(), + + AppDataSource.getRepository(OrgChild2) + .createQueryBuilder("orgChild2") + .where("orgChild2.orgRevisionId = :id", { id }) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany(), + + AppDataSource.getRepository(OrgChild3) + .createQueryBuilder("orgChild3") + .where("orgChild3.orgRevisionId = :id", { id }) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany(), + + AppDataSource.getRepository(OrgChild4) + .createQueryBuilder("orgChild4") + .where("orgChild4.orgRevisionId = :id", { id }) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany(), + ]); + + // Filter child1 data by orgRootIds (maintains backward compatibility) + const orgChild1Data = orgRootIds && orgRootIds.length > 0 + ? orgChild1AllData.filter((orgChild1) => orgRootIds.includes(orgChild1.orgRootId)) + : []; + + // Build maps for efficient filtering of deeper levels + const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id); + const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 + ? orgChild2AllData.filter((orgChild2) => orgChild1Ids.includes(orgChild2.orgChild1Id)) + : []; + + const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id); + const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 + ? orgChild3AllData.filter((orgChild3) => orgChild2Ids.includes(orgChild3.orgChild2Id)) + : []; + + const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id); + const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 + ? orgChild4AllData.filter((orgChild4) => orgChild3Ids.includes(orgChild4.orgChild3Id)) + : []; + + // OPTIMIZED: Build formatted data using pre-calculated counts (no nested queries!) + const formattedData = orgRootData.map((orgRoot) => { + // const rootCounts = getCounts(orgRootMap, orgRoot.id); + // const rootPosCounts = getRootCounts(rootPosMap, orgRoot.id); + + return { + orgTreeId: orgRoot.id, + orgLevel: 0, + orgName: orgRoot.orgRootName, + orgTreeName: orgRoot.orgRootName, + orgTreeShortName: orgRoot.orgRootShortName, + orgTreeCode: orgRoot.orgRootCode, + orgCode: orgRoot.orgRootCode + "00", + orgTreeRank: orgRoot.orgRootRank, + orgTreeRankSub: orgRoot.orgRootRankSub, + orgRootDnaId: orgRoot.ancestorDNA, + DEPARTMENT_CODE: orgRoot.DEPARTMENT_CODE, + DIVISION_CODE: orgRoot.DIVISION_CODE, + SECTION_CODE: orgRoot.SECTION_CODE, + JOB_CODE: orgRoot.JOB_CODE, + orgTreeOrder: orgRoot.orgRootOrder, + orgTreePhoneEx: orgRoot.orgRootPhoneEx, + orgTreePhoneIn: orgRoot.orgRootPhoneIn, + orgTreeFax: orgRoot.orgRootFax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + isDeputy: orgRoot.isDeputy, + isCommission: orgRoot.isCommission, + responsibility: orgRoot.responsibility, + labelName: + orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, + // totalPosition: rootCounts.totalPosition, + // totalPositionCurrentUse: rootCounts.totalPositionCurrentUse, + // totalPositionCurrentVacant: rootCounts.totalPositionCurrentVacant, + // totalPositionNextUse: rootCounts.totalPositionNextUse, + // totalPositionNextVacant: rootCounts.totalPositionNextVacant, + // totalRootPosition: rootPosCounts.totalRootPosition, + // totalRootPositionCurrentUse: rootPosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: rootPosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: rootPosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: rootPosCounts.totalRootPositionNextVacant, + children: orgChild1Data + .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) + .map((orgChild1) => { + // const child1Counts = getCounts(orgChild1Map, orgChild1.id); + // const child1PosKey = `${orgRoot.id}-${orgChild1.id}`; + // const child1PosCounts = getRootCounts(rootPosMap, child1PosKey); + + return { + orgTreeId: orgChild1.id, + orgRootId: orgRoot.id, + orgLevel: 1, + orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild1.orgChild1Name, + orgTreeShortName: orgChild1.orgChild1ShortName, + orgTreeCode: orgChild1.orgChild1Code, + orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code, + orgTreeRank: orgChild1.orgChild1Rank, + orgTreeRankSub: orgChild1.orgChild1RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + DEPARTMENT_CODE: orgChild1.DEPARTMENT_CODE, + DIVISION_CODE: orgChild1.DIVISION_CODE, + SECTION_CODE: orgChild1.SECTION_CODE, + JOB_CODE: orgChild1.JOB_CODE, + orgTreeOrder: orgChild1.orgChild1Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild1.orgChild1PhoneEx, + orgTreePhoneIn: orgChild1.orgChild1PhoneIn, + orgTreeFax: orgChild1.orgChild1Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild1.responsibility, + isOfficer: orgChild1.isOfficer, + isInformation: orgChild1.isInformation, + labelName: + orgChild1.orgChild1Name + + " " + + orgRoot.orgRootCode + + orgChild1.orgChild1Code + + " " + + orgChild1.orgChild1ShortName, + // totalPosition: child1Counts.totalPosition, + // totalPositionCurrentUse: child1Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child1Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child1Counts.totalPositionNextUse, + // totalPositionNextVacant: child1Counts.totalPositionNextVacant, + // totalRootPosition: child1PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: child1PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: child1PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child1PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: child1PosCounts.totalRootPositionNextVacant, + children: orgChild2Data + .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) + .map((orgChild2) => { + // const child2Counts = getCounts(orgChild2Map, orgChild2.id); + // const child2PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}`; + // const child2PosCounts = getRootCounts(rootPosMap, child2PosKey); + + return { + orgTreeId: orgChild2.id, + orgRootId: orgChild1.id, + orgLevel: 2, + orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild2.orgChild2Name, + orgTreeShortName: orgChild2.orgChild2ShortName, + orgTreeCode: orgChild2.orgChild2Code, + orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code, + orgTreeRank: orgChild2.orgChild2Rank, + orgTreeRankSub: orgChild2.orgChild2RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + DEPARTMENT_CODE: orgChild2.DEPARTMENT_CODE, + DIVISION_CODE: orgChild2.DIVISION_CODE, + SECTION_CODE: orgChild2.SECTION_CODE, + JOB_CODE: orgChild2.JOB_CODE, + orgTreeOrder: orgChild2.orgChild2Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild2.orgChild2PhoneEx, + orgTreePhoneIn: orgChild2.orgChild2PhoneIn, + orgTreeFax: orgChild2.orgChild2Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild2.responsibility, + labelName: + orgChild2.orgChild2Name + + " " + + orgRoot.orgRootCode + + orgChild2.orgChild2Code + + " " + + orgChild2.orgChild2ShortName, + // totalPosition: child2Counts.totalPosition, + // totalPositionCurrentUse: child2Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child2Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child2Counts.totalPositionNextUse, + // totalPositionNextVacant: child2Counts.totalPositionNextVacant, + // totalRootPosition: child2PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: child2PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: child2PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child2PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: child2PosCounts.totalRootPositionNextVacant, + children: orgChild3Data + .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) + .map((orgChild3) => { + // const child3Counts = getCounts(orgChild3Map, orgChild3.id); + // const child3PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}`; + // const child3PosCounts = getRootCounts(rootPosMap, child3PosKey); + + return { + orgTreeId: orgChild3.id, + orgRootId: orgChild2.id, + orgLevel: 3, + orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild3.orgChild3Name, + orgTreeShortName: orgChild3.orgChild3ShortName, + orgTreeCode: orgChild3.orgChild3Code, + orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code, + orgTreeRank: orgChild3.orgChild3Rank, + orgTreeRankSub: orgChild3.orgChild3RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + orgChild3DnaId: orgChild3.ancestorDNA, + DEPARTMENT_CODE: orgChild3.DEPARTMENT_CODE, + DIVISION_CODE: orgChild3.DIVISION_CODE, + SECTION_CODE: orgChild3.SECTION_CODE, + JOB_CODE: orgChild3.JOB_CODE, + orgTreeOrder: orgChild3.orgChild3Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild3.orgChild3PhoneEx, + orgTreePhoneIn: orgChild3.orgChild3PhoneIn, + orgTreeFax: orgChild3.orgChild3Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild3.responsibility, + labelName: + orgChild3.orgChild3Name + + " " + + orgRoot.orgRootCode + + orgChild3.orgChild3Code + + " " + + orgChild3.orgChild3ShortName, + // totalPosition: child3Counts.totalPosition, + // totalPositionCurrentUse: child3Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child3Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child3Counts.totalPositionNextUse, + // totalPositionNextVacant: child3Counts.totalPositionNextVacant, + // totalRootPosition: child3PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: child3PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: + // child3PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child3PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: child3PosCounts.totalRootPositionNextVacant, + children: orgChild4Data + .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) + .map((orgChild4) => { + // const child4Counts = getCounts(orgChild4Map, orgChild4.id); + // const child4PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}-${orgChild4.id}`; + // const child4PosCounts = getRootCounts(rootPosMap, child4PosKey); + + return { + orgTreeId: orgChild4.id, + orgRootId: orgChild3.id, + orgLevel: 4, + orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild4.orgChild4Name, + orgTreeShortName: orgChild4.orgChild4ShortName, + orgTreeCode: orgChild4.orgChild4Code, + orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code, + orgTreeRank: orgChild4.orgChild4Rank, + orgTreeRankSub: orgChild4.orgChild4RankSub, + orgRootDnaId: orgRoot.ancestorDNA, + orgChild1DnaId: orgChild1.ancestorDNA, + orgChild2DnaId: orgChild2.ancestorDNA, + orgChild3DnaId: orgChild3.ancestorDNA, + orgChild4DnaId: orgChild4.ancestorDNA, + DEPARTMENT_CODE: orgChild4.DEPARTMENT_CODE, + DIVISION_CODE: orgChild4.DIVISION_CODE, + SECTION_CODE: orgChild4.SECTION_CODE, + JOB_CODE: orgChild4.JOB_CODE, + orgTreeOrder: orgChild4.orgChild4Order, + orgRootCode: orgRoot.orgRootCode, + orgTreePhoneEx: orgChild4.orgChild4PhoneEx, + orgTreePhoneIn: orgChild4.orgChild4PhoneIn, + orgTreeFax: orgChild4.orgChild4Fax, + orgRevisionId: orgRoot.orgRevisionId, + orgRootName: orgRoot.orgRootName, + responsibility: orgChild4.responsibility, + labelName: + orgChild4.orgChild4Name + + " " + + orgRoot.orgRootCode + + orgChild4.orgChild4Code + + " " + + orgChild4.orgChild4ShortName, + // totalPosition: child4Counts.totalPosition, + // totalPositionCurrentUse: child4Counts.totalPositionCurrentUse, + // totalPositionCurrentVacant: child4Counts.totalPositionCurrentVacant, + // totalPositionNextUse: child4Counts.totalPositionNextUse, + // totalPositionNextVacant: child4Counts.totalPositionNextVacant, + // totalRootPosition: child4PosCounts.totalRootPosition, + // totalRootPositionCurrentUse: + // child4PosCounts.totalRootPositionCurrentUse, + // totalRootPositionCurrentVacant: + // child4PosCounts.totalRootPositionCurrentVacant, + // totalRootPositionNextUse: child4PosCounts.totalRootPositionNextUse, + // totalRootPositionNextVacant: + // child4PosCounts.totalRootPositionNextVacant, + children: [], + }; + }), + }; + }), + }; + }), + }; + }), + }; + }); + + return new HttpSuccess(formattedData); + } From 7955c855bc5ef709e2e85596608f1247337252c6 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 29 Jan 2026 00:05:56 +0700 Subject: [PATCH 010/178] fix: extend OrgStructureCache TTL and add graceful shutdown cleanup - Extended OrgStructureCache TTL from 10 to 30 minutes (reduce cleanup frequency) - Added orgStructureCache.destroy() in graceful shutdown handler - Updated documentation to reflect changes Co-Authored-By: Claude (glm-4.7) --- CHANGELOG.md | 5 +++++ docs/SUMMARY_OPTIMIZATION-fix-optimization.md | 10 +++++++--- src/app.ts | 4 ++++ src/utils/OrgStructureCache.ts | 4 ++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4cb1c9..404b5f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ All notable changes to this project will be documented in this file. - แก้ชนิด type ที่ reques +### ⚡ Performance + +- Extended OrgStructureCache TTL from 10 to 30 minutes (reduce cleanup frequency) +- Added OrgStructureCache.destroy() in graceful shutdown handler + ### ⚙️ Miscellaneous Tasks - Git-cliff changelog diff --git a/docs/SUMMARY_OPTIMIZATION-fix-optimization.md b/docs/SUMMARY_OPTIMIZATION-fix-optimization.md index 01dcb2a6..6d4907c7 100644 --- a/docs/SUMMARY_OPTIMIZATION-fix-optimization.md +++ b/docs/SUMMARY_OPTIMIZATION-fix-optimization.md @@ -65,7 +65,11 @@ const gracefulShutdown = async (signal: string) => { // 3. ทำลาย cache instances logMemoryStore.destroy(); + console.log("[APP] LogMemoryStore destroyed"); + + // Destroy OrgStructureCache orgStructureCache.destroy(); + console.log("[APP] OrgStructureCache destroyed"); // 4. บังคับปิดหลังจาก 30 วินาที (หาก shutdown ค้าง) const shutdownTimeout = setTimeout(() => { @@ -134,7 +138,7 @@ const profile = await logMemoryStore.getProfileByKeycloak(keycloak); ```typescript class OrgStructureCache { private cache: Map; - private readonly CACHE_TTL = 10 * 60 * 1000; // 10 นาที + private readonly CACHE_TTL = 30 * 60 * 1000; // 30 นาที // Key format: org-structure-{revisionId}-{rootId} private generateKey(revisionId: string, rootId?: string): string @@ -147,9 +151,9 @@ class OrgStructureCache { **การทำงาน:** - Cache ผลลัพธ์ของ org structure ตาม `revisionId` และ `rootId` -- TTL 10 นาที - ข้อมูลเก่าจะถูกลบอัตโนมัติ +- TTL 30 นาที - ข้อมูลเก่าจะถูกลบอัตโนมัติ (ปรับจาก 10 นาที เพื่อลด cleanup frequency) - Method `invalidate()` - ลบ cache เมื่อมีการอัปเดต revision -- Auto cleanup ทุก 10 นาที +- Auto cleanup ทุก 30 นาที **การใช้งานใน API:** ```typescript diff --git a/src/app.ts b/src/app.ts index a44cace5..578a8e65 100644 --- a/src/app.ts +++ b/src/app.ts @@ -146,6 +146,10 @@ async function main() { logMemoryStore.destroy(); console.log("[APP] LogMemoryStore destroyed"); + // Destroy OrgStructureCache + orgStructureCache.destroy(); + console.log("[APP] OrgStructureCache destroyed"); + clearTimeout(shutdownTimeout); console.log("[APP] Graceful shutdown completed"); process.exit(0); diff --git a/src/utils/OrgStructureCache.ts b/src/utils/OrgStructureCache.ts index 33fa19e2..d4e6c20e 100644 --- a/src/utils/OrgStructureCache.ts +++ b/src/utils/OrgStructureCache.ts @@ -5,7 +5,7 @@ interface CacheEntry { class OrgStructureCache { private cache: Map = new Map(); - private readonly CACHE_TTL = 10 * 60 * 1000; // 10 minutes + private readonly CACHE_TTL = 30 * 60 * 1000; // 30 minutes private isInitialized = false; private cleanupTimer: NodeJS.Timeout | null = null; @@ -17,7 +17,7 @@ class OrgStructureCache { if (this.isInitialized) return; this.isInitialized = true; - // Cleanup expired entries every 10 minutes + // Cleanup expired entries every 30 minutes this.cleanupTimer = setInterval(() => { this.cleanup(); }, this.CACHE_TTL); From 656c2e73415ad87bff3f5c814b38b79cbddd409a Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 29 Jan 2026 00:30:34 +0700 Subject: [PATCH 011/178] Changed LogMemoryStore from active refresh (setInterval) to passive refresh on-access (60 min TTL) --- CHANGELOG.md | 1 + docs/SUMMARY_OPTIMIZATION-fix-optimization.md | 6 ++- src/utils/LogMemoryStore.ts | 47 +++++++++++-------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 404b5f0b..796a107d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. - Extended OrgStructureCache TTL from 10 to 30 minutes (reduce cleanup frequency) - Added OrgStructureCache.destroy() in graceful shutdown handler +- Changed LogMemoryStore from active refresh (setInterval) to passive refresh on-access (60 min TTL) ### ⚙️ Miscellaneous Tasks diff --git a/docs/SUMMARY_OPTIMIZATION-fix-optimization.md b/docs/SUMMARY_OPTIMIZATION-fix-optimization.md index 6d4907c7..d2be25e1 100644 --- a/docs/SUMMARY_OPTIMIZATION-fix-optimization.md +++ b/docs/SUMMARY_OPTIMIZATION-fix-optimization.md @@ -104,15 +104,17 @@ class LogMemoryStore { profileCache: Map, // keycloak → Profile rootIdCache: Map, // profileId → rootId }; - private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 นาที + private readonly CACHE_TTL = 60 * 60 * 1000; // 60 นาที } ``` **การทำงาน:** -- Cache `currentRevision` ทุก 10 นาที +- Passive cache refresh - ตรวจสอบและ refresh cache เมื่อมีการเข้าถึงข้อมูล (on-access) +- หาก cache เก่าเกิน 60 นาที จะทำการ refresh อัตโนมัติ - Lazy load `profileCache` และ `rootIdCache` (โหลดเมื่อถูกเรียกใช้) - Method `getProfileByKeycloak()` - ดึง profile จาก cache หรือ database - Method `getRootIdByProfileId()` - ดึง rootId จาก cache หรือ database +- ไม่มี setInterval (ลดการใช้งาน timer) #### 3.2 Log Middleware (`src/middlewares/logs.ts`) diff --git a/src/utils/LogMemoryStore.ts b/src/utils/LogMemoryStore.ts index 456c06c8..24e84f9e 100644 --- a/src/utils/LogMemoryStore.ts +++ b/src/utils/LogMemoryStore.ts @@ -17,10 +17,8 @@ class LogMemoryStore { rootIdCache: new Map(), updatedAt: new Date(), }; - private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes - private isRefreshing = false; + private readonly CACHE_TTL = 60 * 60 * 1000; // 60 minutes private isInitialized = false; - private refreshTimer: NodeJS.Timeout | null = null; constructor() { // ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready @@ -35,24 +33,15 @@ class LogMemoryStore { this.isInitialized = true; this.refreshCache(); - this.refreshTimer = setInterval(() => { - this.refreshCache(); - }, this.REFRESH_INTERVAL); console.log( "[LogMemoryStore] Initialized with", - this.REFRESH_INTERVAL / 1000, - "second refresh interval", + this.CACHE_TTL / 1000 / 60, + "minute TTL (passive cleanup on access)", ); } private async refreshCache() { - if (this.isRefreshing) { - console.log("[LogMemoryStore] Already refreshing, skipping..."); - return; - } - - this.isRefreshing = true; try { // Refresh revision cache const repoRevision = AppDataSource.getRepository(OrgRevision); @@ -72,12 +61,26 @@ class LogMemoryStore { console.log("[LogMemoryStore] Cache refreshed at", this.cache.updatedAt.toISOString()); } catch (error) { console.error("[LogMemoryStore] Error refreshing cache:", error); - } finally { - this.isRefreshing = false; + } + } + + // Check if cache is stale and refresh if needed + private async checkAndRefreshIfNeeded() { + const now = new Date(); + const age = now.getTime() - this.cache.updatedAt.getTime(); + if (age > this.CACHE_TTL) { + console.log( + "[LogMemoryStore] Cache is stale (age:", + Math.round(age / 1000 / 60), + "minutes), refreshing...", + ); + await this.refreshCache(); } } getCurrentRevision(): OrgRevision | null { + // Check for stale data (fire and forget) + this.checkAndRefreshIfNeeded(); return this.cache.currentRevision; } @@ -89,6 +92,9 @@ class LogMemoryStore { * Get Profile by keycloak ID with caching */ async getProfileByKeycloak(keycloak: string): Promise { + // Check for stale data + await this.checkAndRefreshIfNeeded(); + // Check cache first if (this.cache.profileCache.has(keycloak)) { return this.cache.profileCache.get(keycloak)!; @@ -114,6 +120,9 @@ class LogMemoryStore { async getRootIdByProfileId(profileId: string | undefined): Promise { if (!profileId) return null; + // Check for stale data + await this.checkAndRefreshIfNeeded(); + // Check cache first if (this.cache.rootIdCache.has(profileId)) { return this.cache.rootIdCache.get(profileId)!; @@ -148,10 +157,8 @@ class LogMemoryStore { // สำหรับ shutdown destroy() { - if (this.refreshTimer) { - clearInterval(this.refreshTimer); - this.refreshTimer = null; - } + // No active timer to clear + console.log("[LogMemoryStore] Destroyed"); } } From 328b5b800184e19214a86946cdf9b7e1ec748fa7 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 29 Jan 2026 09:34:12 +0700 Subject: [PATCH 012/178] =?UTF-8?q?Fix=20=E0=B9=80=E0=B8=A1=E0=B8=99?= =?UTF-8?q?=E0=B8=B9=E0=B8=AA=E0=B8=B4=E0=B8=97=E0=B8=98=E0=B8=B4=E0=B9=8C?= =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B9=84=E0=B8=82=E0=B8=82=E0=B9=89?= =?UTF-8?q?=E0=B8=AD=E0=B8=A1=E0=B8=B9=E0=B8=A5=E0=B8=97=E0=B8=B0=E0=B9=80?= =?UTF-8?q?=E0=B8=9A=E0=B8=B5=E0=B8=A2=E0=B8=99=E0=B8=9B=E0=B8=A3=E0=B8=B0?= =?UTF-8?q?=E0=B8=A7=E0=B8=B1=E0=B8=95=E0=B8=B4=E0=B8=95=E0=B8=B3=E0=B9=81?= =?UTF-8?q?=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=87/=E0=B9=80=E0=B8=87?= =?UTF-8?q?=E0=B8=B4=E0=B8=99=E0=B9=80=E0=B8=94=E0=B8=B7=E0=B8=AD=E0=B8=99?= =?UTF-8?q?=20Error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PermissionProfileController.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controllers/PermissionProfileController.ts b/src/controllers/PermissionProfileController.ts index 4503dc3c..9f565828 100644 --- a/src/controllers/PermissionProfileController.ts +++ b/src/controllers/PermissionProfileController.ts @@ -448,13 +448,13 @@ export class PermissionProfileController extends Controller { orgRootId: _data.orgRootId, isCheck: _data.isCheck, isEdit: _data.isEdit, - orgNew: _data.orgRootTree.orgRootName, - avatar: _data.profileTree.avatar, - avatarName: _data.profileTree.avatarName, - prefix: _data.profileTree.prefix, - rank: _data.profileTree.rank, - firstName: _data.profileTree.firstName, - lastName: _data.profileTree.lastName, + orgNew: _data.orgRootTree?.orgRootName, + avatar: _data.profileTree?.avatar, + avatarName: _data.profileTree?.avatarName, + prefix: _data.profileTree?.prefix, + rank: _data.profileTree?.rank, + firstName: _data.profileTree?.firstName, + lastName: _data.profileTree?.lastName, org: (_child4 == null ? "" : _child4 + "\n") + (_child3 == null ? "" : _child3 + "\n") + @@ -462,10 +462,10 @@ export class PermissionProfileController extends Controller { (_child1 == null ? "" : _child1 + "\n") + (_root == null ? "" : _root), posNo: shortName, - position: _data.profileTree.position, - posType: _data.profileTree.posType == null ? null : _data.profileTree.posType.posTypeName, + position: _data.profileTree?.position, + posType: _data.profileTree?.posType == null ? null : _data.profileTree?.posType.posTypeName, posLevel: - _data.profileTree.posLevel == null ? null : _data.profileTree.posLevel.posLevelName, + _data.profileTree?.posLevel == null ? null : _data.profileTree?.posLevel.posLevelName, }; }), ); From 69acf3bb0b3664d791d56beb5f120789c6c7d1cf Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 29 Jan 2026 13:18:21 +0700 Subject: [PATCH 013/178] add api --- .../OrganizationDotnetController.ts | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 99b90bc5..1d4ef30a 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -1912,6 +1912,346 @@ export class OrganizationDotnetController extends Controller { return new HttpSuccess(mapProfile); } + // เพิ่มที่อยู่ปัจจุบัน + ตำแหน่งหัวหน้า + @Get("by-keycloak2/{keycloakId}") + async NewGetProfileByKeycloak2IdAsync(@Path() keycloakId: string) { + /* ========================= + * 1. Load profile + * ========================= */ + const profile = await this.profileRepo.findOne({ + where: { keycloak: keycloakId }, + relations: { + posLevel: true, + posType: true, + currentProvince: true, + currentDistrict: true, + currentSubDistrict: true, + current_holders: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }, + }); + let commanderFullname = ""; + let commanderPositionName = ""; + let commanderId = ""; + // Employee + if (!profile) { + const profile = await this.profileEmpRepo.findOne({ + where: { keycloak: keycloakId }, + relations: { + posLevel: true, + posType: true, + currentProvince: true, + currentDistrict: true, + currentSubDistrict: true, + current_holders: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }, + }); + if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const currentHolder = profile.current_holders?.find( + x => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + const pos = await this.empPosMasterRepository + .createQueryBuilder("pos") + .leftJoinAndSelect("pos.current_holder", "holder") + .leftJoin("pos.orgRevision", "rev") + .where("rev.orgRevisionIsCurrent = true") + .andWhere("rev.orgRevisionIsDraft = false") + .andWhere("pos.isDirector = true") + .andWhere("pos.current_holderId IS NOT NULL") + .andWhere("pos.orgRootId = :root", { root: currentHolder?.orgRootId }) + .andWhere( + `(pos.orgChild1Id = :c1 OR pos.orgChild1Id IS NULL) + AND (pos.orgChild2Id = :c2 OR pos.orgChild2Id IS NULL) + AND (pos.orgChild3Id = :c3 OR pos.orgChild3Id IS NULL) + AND (pos.orgChild4Id = :c4 OR pos.orgChild4Id IS NULL)`, + { + c1: currentHolder?.orgChild1Id, + c2: currentHolder?.orgChild2Id, + c3: currentHolder?.orgChild3Id, + c4: currentHolder?.orgChild4Id, + }, + ) + .orderBy( + `(CASE WHEN pos.orgChild4Id IS NULL THEN 1 ELSE 0 END) + + (CASE WHEN pos.orgChild3Id IS NULL THEN 1 ELSE 0 END) + + (CASE WHEN pos.orgChild2Id IS NULL THEN 1 ELSE 0 END) + + (CASE WHEN pos.orgChild1Id IS NULL THEN 1 ELSE 0 END)`, + "ASC", + ) + .getOne(); + + if (pos?.current_holder) { + commanderFullname = + `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; + commanderPositionName = pos.current_holder.position; + commanderId = pos.current_holder.id; + } + + let oc = ""; + if (currentHolder) { + if (!currentHolder.orgChild1Id) { + oc = currentHolder.orgRoot?.orgRootName; + } else if (!currentHolder.orgChild2Id) { + oc = `${currentHolder.orgChild1?.orgChild1Name} ${currentHolder.orgRoot?.orgRootName}`; + } else if (!currentHolder.orgChild3Id) { + oc = `${currentHolder.orgChild2?.orgChild2Name} ${currentHolder.orgChild1?.orgChild1Name} ${currentHolder.orgRoot?.orgRootName}`; + } else if (!currentHolder.orgChild4Id) { + oc = `${currentHolder.orgChild3?.orgChild3Name} ${currentHolder.orgChild2?.orgChild2Name} ${currentHolder.orgChild1?.orgChild1Name} ${currentHolder.orgRoot?.orgRootName}`; + } else { + oc = currentHolder.orgChild4?.orgChild4Name; + } + } + + const mapProfile = { + profileType: "EMPLOYEE", + id: profile.id, + keycloak: profile.keycloak, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + position: profile.position, + email: profile.email, + phone: profile.phone, + isProbation: profile.isProbation, + isLeave: profile.isLeave, + dateRetire: profile.dateRetire, + dateAppoint: profile.dateAppoint, + dateRetireLaw: profile.dateRetireLaw, + dateStart: profile.dateStart, + govAgeAbsent: profile.govAgeAbsent, + govAgePlus: profile.govAgePlus, + birthDate: profile.birthDate ?? new Date(), + reasonSameDate: profile.reasonSameDate, + telephoneNumber: profile.phone, + nationality: profile.nationality, + gender: profile.gender, + relationship: profile.relationship, + religion: profile.religion, + bloodGroup: profile.bloodGroup, + dutyTimeId: profile.dutyTimeId, + dutyTimeEffectiveDate: profile.dutyTimeEffectiveDate, + amount: profile.amount, + positionSalaryAmount: profile.positionSalaryAmount, + mouthSalaryAmount: profile.mouthSalaryAmount, + + posType: profile.posType?.posTypeName ?? null, + posLevel: profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null + ? null + : `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, + oc, + + currentAddress: profile.currentAddress, + currentSubDistrict: profile.currentSubDistrict?.name ?? null, + currentDistrict: profile.currentDistrict?.name ?? null, + currentProvince: profile.currentProvince?.name ?? null, + currentZipCode: profile.currentZipCode, + + commander: commanderFullname, + commanderPositionName, + commanderId, + + root: currentHolder?.orgRoot?.orgRootName ?? null, + rootId: currentHolder?.orgRootId ?? null, + rootDnaId: currentHolder?.orgRoot?.ancestorDNA ?? null, + child1: currentHolder?.orgChild1?.orgChild1Name ?? null, + child1Id: currentHolder?.orgChild1Id ?? null, + child1DnaId: currentHolder?.orgChild1?.ancestorDNA ?? null, + child2: currentHolder?.orgChild2?.orgChild2Name ?? null, + child2Id: currentHolder?.orgChild2Id ?? null, + child2DnaId: currentHolder?.orgChild2?.ancestorDNA ?? null, + child3: currentHolder?.orgChild3?.orgChild3Name ?? null, + child3Id: currentHolder?.orgChild3Id ?? null, + child3DnaId: currentHolder?.orgChild3?.ancestorDNA ?? null, + child4: currentHolder?.orgChild4?.orgChild4Name ?? null, + child4Id: currentHolder?.orgChild4Id ?? null, + child4DnaId: currentHolder?.orgChild4?.ancestorDNA ?? null, + }; + + return new HttpSuccess(mapProfile); + } + + /* ========================================= + * 2. current holder + * ========================================= */ + const currentHolder = profile.current_holders?.find( + x => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + /* ================================================= + * 3. หา commander + * ================================================= */ + const pos = await this.posMasterRepository + .createQueryBuilder("pos") + .leftJoinAndSelect("pos.current_holder", "holder") + .leftJoin("pos.orgRevision", "rev") + .where("rev.orgRevisionIsCurrent = true") + .andWhere("rev.orgRevisionIsDraft = false") + .andWhere("pos.isDirector = true") + .andWhere("pos.current_holderId IS NOT NULL") + .andWhere("pos.orgRootId = :root", { root: currentHolder?.orgRootId }) + .andWhere( + `(pos.orgChild1Id = :c1 OR pos.orgChild1Id IS NULL) + AND (pos.orgChild2Id = :c2 OR pos.orgChild2Id IS NULL) + AND (pos.orgChild3Id = :c3 OR pos.orgChild3Id IS NULL) + AND (pos.orgChild4Id = :c4 OR pos.orgChild4Id IS NULL)`, + { + c1: currentHolder?.orgChild1Id, + c2: currentHolder?.orgChild2Id, + c3: currentHolder?.orgChild3Id, + c4: currentHolder?.orgChild4Id, + }, + ) + .orderBy( + `(CASE WHEN pos.orgChild4Id IS NULL THEN 1 ELSE 0 END) + + (CASE WHEN pos.orgChild3Id IS NULL THEN 1 ELSE 0 END) + + (CASE WHEN pos.orgChild2Id IS NULL THEN 1 ELSE 0 END) + + (CASE WHEN pos.orgChild1Id IS NULL THEN 1 ELSE 0 END)`, + "ASC", + ) + .getOne(); + + if (pos?.current_holder) { + commanderFullname = + `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; + commanderPositionName = pos.current_holder.position; + commanderId = pos.current_holder.id; + } + + /* ========================================= + * 5. position executive + * ========================================= */ + const position = await this.positionRepository.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: currentHolder?.orgRevisionId, + current_holderId: profile.id, + }, + }, + order: { createdAt: "DESC" }, + relations: { posExecutive: true }, + }); + + /* ========================================= + * 6. OC name + * ========================================= */ + let oc = ""; + if (currentHolder) { + if (!currentHolder.orgChild1Id) { + oc = currentHolder.orgRoot?.orgRootName; + } else if (!currentHolder.orgChild2Id) { + oc = `${currentHolder.orgChild1?.orgChild1Name} ${currentHolder.orgRoot?.orgRootName}`; + } else if (!currentHolder.orgChild3Id) { + oc = `${currentHolder.orgChild2?.orgChild2Name} ${currentHolder.orgChild1?.orgChild1Name} ${currentHolder.orgRoot?.orgRootName}`; + } else if (!currentHolder.orgChild4Id) { + oc = `${currentHolder.orgChild3?.orgChild3Name} ${currentHolder.orgChild2?.orgChild2Name} ${currentHolder.orgChild1?.orgChild1Name} ${currentHolder.orgRoot?.orgRootName}`; + } else { + oc = currentHolder.orgChild4?.orgChild4Name; + } + } + + /* ========================================= + * 7. position level name + * ========================================= */ + const positionLeaveName = + profile.posType && + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || + profile.posType.posTypeName === "อำนวยการ") + ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` + : profile.posLevel?.posLevelName ?? null; + + /* ========================================= + * 8. map response + * ========================================= */ + const mapProfile = { + profileType: "OFFICER", + id: profile.id, + keycloak: profile.keycloak, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + position: profile.position, + email: profile.email, + phone: profile.phone, + isProbation: profile.isProbation, + isLeave: profile.isLeave, + dateRetire: profile.dateRetire, + dateAppoint: profile.dateAppoint, + dateRetireLaw: profile.dateRetireLaw, + dateStart: profile.dateStart, + govAgeAbsent: profile.govAgeAbsent, + govAgePlus: profile.govAgePlus, + birthDate: profile.birthDate ?? new Date(), + reasonSameDate: profile.reasonSameDate, + telephoneNumber: profile.phone, + nationality: profile.nationality, + gender: profile.gender, + relationship: profile.relationship, + religion: profile.religion, + bloodGroup: profile.bloodGroup, + dutyTimeId: profile.dutyTimeId, + dutyTimeEffectiveDate: profile.dutyTimeEffectiveDate, + amount: profile.amount, + positionSalaryAmount: profile.positionSalaryAmount, + mouthSalaryAmount: profile.mouthSalaryAmount, + + posLevel: profile.posLevel?.posLevelName ?? null, + posType: profile.posType?.posTypeName ?? null, + posExecutiveName: position?.posExecutive?.posExecutiveName ?? null, + positionLeaveName, + oc, + + currentAddress: profile.currentAddress, + currentSubDistrict: profile.currentSubDistrict?.name ?? null, + currentDistrict: profile.currentDistrict?.name ?? null, + currentProvince: profile.currentProvince?.name ?? null, + currentZipCode: profile.currentZipCode, + + commander: commanderFullname, + commanderPositionName, + commanderId, + + root: currentHolder?.orgRoot?.orgRootName ?? null, + rootId: currentHolder?.orgRootId ?? null, + rootDnaId: currentHolder?.orgRoot?.ancestorDNA ?? null, + child1: currentHolder?.orgChild1?.orgChild1Name ?? null, + child1Id: currentHolder?.orgChild1Id ?? null, + child1DnaId: currentHolder?.orgChild1?.ancestorDNA ?? null, + child2: currentHolder?.orgChild2?.orgChild2Name ?? null, + child2Id: currentHolder?.orgChild2Id ?? null, + child2DnaId: currentHolder?.orgChild2?.ancestorDNA ?? null, + child3: currentHolder?.orgChild3?.orgChild3Name ?? null, + child3Id: currentHolder?.orgChild3Id ?? null, + child3DnaId: currentHolder?.orgChild3?.ancestorDNA ?? null, + child4: currentHolder?.orgChild4?.orgChild4Name ?? null, + child4Id: currentHolder?.orgChild4Id ?? null, + child4DnaId: currentHolder?.orgChild4?.ancestorDNA ?? null, + }; + + return new HttpSuccess(mapProfile); + } + /** * API Get Profile For Logs * From d36c4c931c10671a74d68b3e5a741d1611ce4ace Mon Sep 17 00:00:00 2001 From: Adisak Date: Thu, 29 Jan 2026 14:10:11 +0700 Subject: [PATCH 014/178] #2259 --- src/controllers/WorkflowController.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index f02aaf4b..3bd6fe19 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -1045,11 +1045,11 @@ export class WorkflowController extends Controller { const processedData = body.isAct ? data : data.map((x: any) => ({ - ...x, - posExecutiveNameOrg: - (x.posExecutiveName ?? "") + - (x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""), - })); + ...x, + posExecutiveNameOrg: + (x.posExecutiveName ?? "") + + (x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""), + })); return new HttpSuccess({ data: processedData, total }); } @@ -1363,7 +1363,7 @@ export class WorkflowController extends Controller { keycloak: req.user.sub, }, }); - if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); + if (!profile) throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบข้อมูลผู้ใช้งาน"); const profileOfficer = await this.workflowRepo.findOne({ where: { @@ -1372,7 +1372,7 @@ export class WorkflowController extends Controller { // sysName: body.sysName, }, }); - if (!profileOfficer) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); + if (!profileOfficer) throw new HttpError(HttpStatus.FORBIDDEN, "ไม่พบข้อมูลสิทธิ์"); return new HttpSuccess(); } From 12d2eb1ee9441f93baa0f22fa171182a9d276b5f Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 30 Jan 2026 10:20:54 +0700 Subject: [PATCH 015/178] =?UTF-8?q?=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1?= =?UTF-8?q?=E0=B8=B9=E0=B8=A5=E0=B8=97=E0=B8=B0=E0=B9=80=E0=B8=9A=E0=B8=B5?= =?UTF-8?q?=E0=B8=A2=E0=B8=99=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B8=A7=E0=B8=B1?= =?UTF-8?q?=E0=B8=95=E0=B8=B4=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=AD=E0=B8=B1?= =?UTF-8?q?=E0=B8=9B=E0=B9=80=E0=B8=94=E0=B8=95=E0=B8=AB=E0=B8=A5=E0=B8=B1?= =?UTF-8?q?=E0=B8=87=20"=E0=B8=A2=E0=B8=B7=E0=B8=99=E0=B8=A2=E0=B8=B1?= =?UTF-8?q?=E0=B8=99=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=B9=E0=B8=A5?= =?UTF-8?q?=E0=B8=96=E0=B8=B9=E0=B8=81=E0=B8=95=E0=B9=89=E0=B8=AD=E0=B8=87?= =?UTF-8?q?"=20#2243?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProfileSalaryTempController.ts | 95 ++++++++++++++++--- 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index cbdcf50a..e4cb6637 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1290,24 +1290,97 @@ export class ProfileSalaryTempController extends Controller { @Request() req: RequestWithUser, @Body() body: { profileId: string; type: string }, ) { - if (body.type.toLocaleUpperCase() == "OFFICER") { - const profile = await this.profileRepo.findOneBy({ id: body.profileId }); + const queryRunner = AppDataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const isOfficer = body.type.toUpperCase() === "OFFICER"; + + /* ========================= + * 1. Load Profile + * ========================= */ + const profile = isOfficer + ? await queryRunner.manager.findOne(Profile, { where: { id: body.profileId } }) + : await queryRunner.manager.findOne(ProfileEmployee, { where: { id: body.profileId } }); + if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบข้อมูล profile"); } + profile.statusCheckEdit = "CHECKED"; - await this.profileRepo.save(profile); - } else { - const profile = await this.profileEmployeeRepo.findOneBy({ id: body.profileId }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + await queryRunner.manager.save(profile); + + /* ========================= + * 2. Load Salary Temp + * ========================= */ + const salaryTemps = await queryRunner.manager.find(ProfileSalaryTemp, { + where: isOfficer + ? { profileId: body.profileId } + : { profileEmployeeId: body.profileId }, + }); + + if (salaryTemps.length === 0) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลตำแหน่ง/เงินเดือนของ profile นี้"); } - profile.statusCheckEdit = "CHECKED"; - await this.profileEmployeeRepo.save(profile); + + /* ========================= + * 3. Split Update / Insert + * ========================= */ + const toUpdate = salaryTemps.filter(t => t.salaryId); + const toInsert = salaryTemps.filter(t => !t.salaryId); + const dateNow = new Date(); + const metaUpdate = { + lastUpdateUserId: req.user.sub, + lastUpdateFullName: req.user.name, + lastUpdatedAt: dateNow, + }; + + /* ========================= + * 4. UPDATE + * ========================= */ + for (const temp of toUpdate) { + const { salaryId, id, ...data } = temp; + await queryRunner.manager.update( + ProfileSalary, + { id: salaryId }, + { + ...data, + ...metaUpdate, + }, + ); + } + + /* ========================= + * 5. INSERT (bulk) + * ========================= */ + if (toInsert.length > 0) { + const metaCreate = { + createdUserId: req.user.sub, + createdFullName: req.user.name, + createdAt: dateNow, + }; + const insertData = toInsert.map(({ id, ...data }) => ({ + ...data, + ...metaCreate, + ...metaUpdate, + })); + + await queryRunner.manager.insert(ProfileSalary, insertData); + } + + await queryRunner.commitTransaction(); + return new HttpSuccess(); + + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); } - return new HttpSuccess(); } + /** * API แก้ไขข้อมูล * From e461f43604a4ee50a1ed2866eb52bc4ea903866b Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 30 Jan 2026 12:01:38 +0700 Subject: [PATCH 016/178] Fix Bug #2243 --- src/controllers/ProfileSalaryTempController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index e4cb6637..9e458953 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1340,7 +1340,7 @@ export class ProfileSalaryTempController extends Controller { * 4. UPDATE * ========================= */ for (const temp of toUpdate) { - const { salaryId, id, ...data } = temp; + const { id, salaryId, isDelete, isEdit, ...data } = temp; await queryRunner.manager.update( ProfileSalary, { id: salaryId }, From 633ccd4906edca8ee694e66c7d9959e88623be05 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 30 Jan 2026 14:47:46 +0700 Subject: [PATCH 017/178] fix:delete profileAvatar --- src/controllers/ProfileAvatarController.ts | 22 +++++++++++++++++++ .../ProfileAvatarEmployeeController.ts | 14 ++++++++++++ .../ProfileAvatarEmployeeTempController.ts | 14 ++++++++++++ src/interfaces/call-api.ts | 17 ++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index dc8a7e0f..16537b06 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -8,6 +8,7 @@ import { Profile } from "../entities/Profile"; import { CreateProfileAvatar, ProfileAvatar } from "../entities/ProfileAvatar"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import CallAPI from "../interfaces/call-api"; @Route("api/v1/org/profile/avatar") @Tags("ProfileAvatar") @Security("bearerAuth") @@ -158,12 +159,33 @@ export class ProfileAvatarController extends Controller { "SYS_REGISTRY_OFFICER", _record.profileId, ); + if (_record.isActive) { + const profile = await this.profileRepository.findOne({ + where: { id: _record.profileId }, + }); + if (profile) { + profile.avatar = ""; + profile.avatarName = ""; + await this.profileRepository.save(profile, { data: req }); + } + } + + await new CallAPI().DeleteFile(req, `${_record?.avatar}/${_record?.avatarName}`); } if (!_record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); } + await this.avatarRepository.remove(_record, { data: req }); return new HttpSuccess(); } + + // private async deleteFile(avatar: string, avatarName: string) { + // const url = process.env.API_URL + `/salary/file/${avatar}/${avatarName}`; + // console.log(url); + // try { + // await http.delete(url); + // } catch {} + // } } diff --git a/src/controllers/ProfileAvatarEmployeeController.ts b/src/controllers/ProfileAvatarEmployeeController.ts index 21f9f536..72c652f1 100644 --- a/src/controllers/ProfileAvatarEmployeeController.ts +++ b/src/controllers/ProfileAvatarEmployeeController.ts @@ -8,6 +8,8 @@ import { CreateProfileEmployeeAvatar, ProfileAvatar } from "../entities/ProfileA import { ProfileEmployee } from "../entities/ProfileEmployee"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import CallAPI from "../interfaces/call-api"; + @Route("api/v1/org/profile-employee/avatar") @Tags("ProfileAvatar") @Security("bearerAuth") @@ -153,6 +155,18 @@ export class ProfileAvatarEmployeeController extends Controller { "SYS_REGISTRY_EMP", _record.profileEmployeeId, ); + if (_record.isActive) { + const profile = await this.profileRepository.findOne({ + where: { id: _record.profileEmployeeId }, + }); + if (profile) { + profile.avatar = ""; + profile.avatarName = ""; + await this.profileRepository.save(profile, { data: req }); + } + } + + await new CallAPI().DeleteFile(req, `${_record?.avatar}/${_record?.avatarName}`); } if (!_record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); diff --git a/src/controllers/ProfileAvatarEmployeeTempController.ts b/src/controllers/ProfileAvatarEmployeeTempController.ts index f16944a9..e26bd53f 100644 --- a/src/controllers/ProfileAvatarEmployeeTempController.ts +++ b/src/controllers/ProfileAvatarEmployeeTempController.ts @@ -8,6 +8,7 @@ import { CreateProfileEmployeeAvatar, ProfileAvatar } from "../entities/ProfileA import { ProfileEmployee } from "../entities/ProfileEmployee"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import CallAPI from "../interfaces/call-api"; @Route("api/v1/org/profile-temp/avatar") @Tags("ProfileAvatar") @Security("bearerAuth") @@ -147,6 +148,19 @@ export class ProfileAvatarEmployeeTempController extends Controller { public async deleteAvatarEmployee(@Path() avatarId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); const _record = await this.avatarRepository.findOneBy({ id: avatarId }); + if (_record) { + if (_record.isActive) { + const profile = await this.profileRepository.findOne({ + where: { id: _record.profileEmployeeId }, + }); + if (profile) { + profile.avatar = ""; + profile.avatarName = ""; + await this.profileRepository.save(profile, { data: req }); + } + } + } + await new CallAPI().DeleteFile(req, `${_record?.avatar}/${_record?.avatarName}`); if (!_record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); } diff --git a/src/interfaces/call-api.ts b/src/interfaces/call-api.ts index 3639c37c..398246c9 100644 --- a/src/interfaces/call-api.ts +++ b/src/interfaces/call-api.ts @@ -99,6 +99,23 @@ class CallAPI { throw error; } } + + //Delete File + public async DeleteFile(request: any, @Path() path: any) { + const token = "Bearer " + request.headers.authorization.replace("Bearer ", ""); + const url = process.env.API_URL + "/salary/file/" + path; + try { + await axios.delete(url, { + headers: { + Authorization: `${token}`, + "Content-Type": "application/json", + api_key: process.env.API_KEY, + }, + }); + } catch (error) { + throw error; + } + } } export default CallAPI; From 109caf7a0d8c2ebf77ab166f9a1d58aea0ca75eb Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 30 Jan 2026 16:15:26 +0700 Subject: [PATCH 018/178] fix: Type ProfileAvatar string | null --- src/controllers/ProfileAvatarController.ts | 12 ++---------- src/controllers/ProfileAvatarEmployeeController.ts | 4 ++-- .../ProfileAvatarEmployeeTempController.ts | 4 ++-- src/entities/Profile.ts | 4 ++-- src/entities/ProfileEmployee.ts | 8 ++++---- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index 16537b06..717f65f5 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -164,8 +164,8 @@ export class ProfileAvatarController extends Controller { where: { id: _record.profileId }, }); if (profile) { - profile.avatar = ""; - profile.avatarName = ""; + profile.avatar = null; + profile.avatarName = null; await this.profileRepository.save(profile, { data: req }); } } @@ -180,12 +180,4 @@ export class ProfileAvatarController extends Controller { return new HttpSuccess(); } - - // private async deleteFile(avatar: string, avatarName: string) { - // const url = process.env.API_URL + `/salary/file/${avatar}/${avatarName}`; - // console.log(url); - // try { - // await http.delete(url); - // } catch {} - // } } diff --git a/src/controllers/ProfileAvatarEmployeeController.ts b/src/controllers/ProfileAvatarEmployeeController.ts index 72c652f1..ac6c2ff0 100644 --- a/src/controllers/ProfileAvatarEmployeeController.ts +++ b/src/controllers/ProfileAvatarEmployeeController.ts @@ -160,8 +160,8 @@ export class ProfileAvatarEmployeeController extends Controller { where: { id: _record.profileEmployeeId }, }); if (profile) { - profile.avatar = ""; - profile.avatarName = ""; + profile.avatar = null; + profile.avatarName = null; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/controllers/ProfileAvatarEmployeeTempController.ts b/src/controllers/ProfileAvatarEmployeeTempController.ts index e26bd53f..da4a109f 100644 --- a/src/controllers/ProfileAvatarEmployeeTempController.ts +++ b/src/controllers/ProfileAvatarEmployeeTempController.ts @@ -154,8 +154,8 @@ export class ProfileAvatarEmployeeTempController extends Controller { where: { id: _record.profileEmployeeId }, }); if (profile) { - profile.avatar = ""; - profile.avatarName = ""; + profile.avatar = null; + profile.avatarName = null; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index 994fcfbe..aae4273f 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -58,14 +58,14 @@ export class Profile extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string; + avatar: string | null; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string; + avatarName: string | null; @Column({ nullable: true, diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index 04f7923f..6db7190f 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -46,14 +46,14 @@ export class ProfileEmployee extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string; + avatar: string | null; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string; + avatarName: string | null; @Column({ nullable: true, @@ -693,13 +693,13 @@ export class ProfileEmployee extends EntityBase { default: false, }) privacyCheckin: boolean; - + @Column({ comment: "สถานะยืนยัน privacyUser", default: false, }) privacyUser: boolean; - + @Column({ comment: "สถานะยืนยัน privacyMgt", default: false, From 79372e803cb1431531ada81f7e3117b9aa47e8df Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 30 Jan 2026 17:05:16 +0700 Subject: [PATCH 019/178] fix: avatar : string --- src/controllers/ProfileAvatarController.ts | 4 ++-- src/controllers/ProfileAvatarEmployeeController.ts | 4 ++-- src/controllers/ProfileAvatarEmployeeTempController.ts | 4 ++-- src/entities/Profile.ts | 4 ++-- src/entities/ProfileEmployee.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index 717f65f5..85bc4adc 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -164,8 +164,8 @@ export class ProfileAvatarController extends Controller { where: { id: _record.profileId }, }); if (profile) { - profile.avatar = null; - profile.avatarName = null; + profile.avatar = ''; + profile.avatarName = ''; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/controllers/ProfileAvatarEmployeeController.ts b/src/controllers/ProfileAvatarEmployeeController.ts index ac6c2ff0..e36b699a 100644 --- a/src/controllers/ProfileAvatarEmployeeController.ts +++ b/src/controllers/ProfileAvatarEmployeeController.ts @@ -160,8 +160,8 @@ export class ProfileAvatarEmployeeController extends Controller { where: { id: _record.profileEmployeeId }, }); if (profile) { - profile.avatar = null; - profile.avatarName = null; + profile.avatar = ''; + profile.avatarName = ''; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/controllers/ProfileAvatarEmployeeTempController.ts b/src/controllers/ProfileAvatarEmployeeTempController.ts index da4a109f..e26bd53f 100644 --- a/src/controllers/ProfileAvatarEmployeeTempController.ts +++ b/src/controllers/ProfileAvatarEmployeeTempController.ts @@ -154,8 +154,8 @@ export class ProfileAvatarEmployeeTempController extends Controller { where: { id: _record.profileEmployeeId }, }); if (profile) { - profile.avatar = null; - profile.avatarName = null; + profile.avatar = ""; + profile.avatarName = ""; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index aae4273f..994fcfbe 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -58,14 +58,14 @@ export class Profile extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string | null; + avatar: string; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string | null; + avatarName: string; @Column({ nullable: true, diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index 6db7190f..e358304d 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -46,14 +46,14 @@ export class ProfileEmployee extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string | null; + avatar: string; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string | null; + avatarName: string; @Column({ nullable: true, From cd684789456f6b4db745c373131d135e36053b53 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 30 Jan 2026 17:06:23 +0700 Subject: [PATCH 020/178] fix:avatar:string --- src/controllers/ProfileAvatarController.ts | 4 ++-- src/controllers/ProfileAvatarEmployeeController.ts | 4 ++-- src/controllers/ProfileAvatarEmployeeTempController.ts | 4 ++-- src/entities/Profile.ts | 4 ++-- src/entities/ProfileEmployee.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index 717f65f5..d9a6e1f9 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -164,8 +164,8 @@ export class ProfileAvatarController extends Controller { where: { id: _record.profileId }, }); if (profile) { - profile.avatar = null; - profile.avatarName = null; + profile.avatar = ""; + profile.avatarName = ""; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/controllers/ProfileAvatarEmployeeController.ts b/src/controllers/ProfileAvatarEmployeeController.ts index ac6c2ff0..72c652f1 100644 --- a/src/controllers/ProfileAvatarEmployeeController.ts +++ b/src/controllers/ProfileAvatarEmployeeController.ts @@ -160,8 +160,8 @@ export class ProfileAvatarEmployeeController extends Controller { where: { id: _record.profileEmployeeId }, }); if (profile) { - profile.avatar = null; - profile.avatarName = null; + profile.avatar = ""; + profile.avatarName = ""; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/controllers/ProfileAvatarEmployeeTempController.ts b/src/controllers/ProfileAvatarEmployeeTempController.ts index da4a109f..e26bd53f 100644 --- a/src/controllers/ProfileAvatarEmployeeTempController.ts +++ b/src/controllers/ProfileAvatarEmployeeTempController.ts @@ -154,8 +154,8 @@ export class ProfileAvatarEmployeeTempController extends Controller { where: { id: _record.profileEmployeeId }, }); if (profile) { - profile.avatar = null; - profile.avatarName = null; + profile.avatar = ""; + profile.avatarName = ""; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index aae4273f..994fcfbe 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -58,14 +58,14 @@ export class Profile extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string | null; + avatar: string; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string | null; + avatarName: string; @Column({ nullable: true, diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index 6db7190f..e358304d 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -46,14 +46,14 @@ export class ProfileEmployee extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string | null; + avatar: string; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string | null; + avatarName: string; @Column({ nullable: true, From 84fb85ef3ae0bfd07ef84f8ff523516d486ff036 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 30 Jan 2026 17:22:25 +0700 Subject: [PATCH 021/178] fix: test entity avatar --- src/controllers/ProfileAvatarController.ts | 4 ++-- src/entities/Profile.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index d9a6e1f9..717f65f5 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -164,8 +164,8 @@ export class ProfileAvatarController extends Controller { where: { id: _record.profileId }, }); if (profile) { - profile.avatar = ""; - profile.avatarName = ""; + profile.avatar = null; + profile.avatarName = null; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index 994fcfbe..aae4273f 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -58,14 +58,14 @@ export class Profile extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string; + avatar: string | null; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string; + avatarName: string | null; @Column({ nullable: true, From 510aaee0ee527d985f7b56aa211c68cbf6091f76 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 30 Jan 2026 17:30:34 +0700 Subject: [PATCH 022/178] fix --- src/controllers/ProfileAvatarController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index 717f65f5..73374070 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -159,6 +159,7 @@ export class ProfileAvatarController extends Controller { "SYS_REGISTRY_OFFICER", _record.profileId, ); + if (_record.isActive) { const profile = await this.profileRepository.findOne({ where: { id: _record.profileId }, From 89a34f38efda133d160f0c83ab74e79da63e6ca6 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 30 Jan 2026 18:20:13 +0700 Subject: [PATCH 023/178] fix avatar? --- src/entities/Profile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index aae4273f..b1e99e9f 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -58,14 +58,14 @@ export class Profile extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar: string | null; + avatar?: string | null; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName: string | null; + avatarName?: string | null; @Column({ nullable: true, From 74752361be00b4a49a350e7d945435288c94d918 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 30 Jan 2026 18:22:07 +0700 Subject: [PATCH 024/178] fix: avatar : string --- src/controllers/ProfileAvatarController.ts | 4 ++-- src/entities/Profile.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index 73374070..099cdcb1 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -165,8 +165,8 @@ export class ProfileAvatarController extends Controller { where: { id: _record.profileId }, }); if (profile) { - profile.avatar = null; - profile.avatarName = null; + profile.avatar = ""; + profile.avatarName = ""; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index b1e99e9f..f5bdd6ce 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -58,14 +58,14 @@ export class Profile extends EntityBase { comment: "รูปถ่าย", default: null, }) - avatar?: string | null; + avatar: string ; @Column({ nullable: true, comment: "รูปถ่าย", default: null, }) - avatarName?: string | null; + avatarName: string; @Column({ nullable: true, From e92321d3606a190e1bf08b599ab7e11748466f0f Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Sat, 31 Jan 2026 12:37:25 +0700 Subject: [PATCH 025/178] fix: type avatar & avatarName, clear value null --- src/controllers/ProfileAvatarController.ts | 4 ++-- src/controllers/ProfileAvatarEmployeeController.ts | 4 ++-- src/entities/Profile.ts | 6 ++++-- src/entities/ProfileEmployee.ts | 6 ++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/controllers/ProfileAvatarController.ts b/src/controllers/ProfileAvatarController.ts index 099cdcb1..73374070 100644 --- a/src/controllers/ProfileAvatarController.ts +++ b/src/controllers/ProfileAvatarController.ts @@ -165,8 +165,8 @@ export class ProfileAvatarController extends Controller { where: { id: _record.profileId }, }); if (profile) { - profile.avatar = ""; - profile.avatarName = ""; + profile.avatar = null; + profile.avatarName = null; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/controllers/ProfileAvatarEmployeeController.ts b/src/controllers/ProfileAvatarEmployeeController.ts index 72c652f1..ac6c2ff0 100644 --- a/src/controllers/ProfileAvatarEmployeeController.ts +++ b/src/controllers/ProfileAvatarEmployeeController.ts @@ -160,8 +160,8 @@ export class ProfileAvatarEmployeeController extends Controller { where: { id: _record.profileEmployeeId }, }); if (profile) { - profile.avatar = ""; - profile.avatarName = ""; + profile.avatar = null; + profile.avatarName = null; await this.profileRepository.save(profile, { data: req }); } } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index f5bdd6ce..8fc1275f 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -57,15 +57,17 @@ export class Profile extends EntityBase { nullable: true, comment: "รูปถ่าย", default: null, + type: "varchar", }) - avatar: string ; + avatar: string | null; @Column({ nullable: true, comment: "รูปถ่าย", default: null, + type: "varchar", }) - avatarName: string; + avatarName: string | null; @Column({ nullable: true, diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index e358304d..e73ca914 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -45,15 +45,17 @@ export class ProfileEmployee extends EntityBase { nullable: true, comment: "รูปถ่าย", default: null, + type: "varchar", }) - avatar: string; + avatar: string | null; @Column({ nullable: true, comment: "รูปถ่าย", default: null, + type: "varchar", }) - avatarName: string; + avatarName: string | null; @Column({ nullable: true, From 8b46a2f0f26b4ff5485ba2be73e25179e6d85e01 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 2 Feb 2026 09:25:13 +0700 Subject: [PATCH 026/178] #2166 --- src/services/rabbitmq.ts | 53 ++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 326cb9a1..8e9f11f4 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -85,7 +85,7 @@ export async function init() { console.log("[AMQ] Listening for message..."); createConsumer(queue, channel, handler), //----> (3) Process Consumer - createConsumer(queue_org, channel, handler_org); + createConsumer(queue_org, channel, handler_org); createConsumer(queue_org_draft, channel, handler_org_draft); createConsumer(queue_command_noti, channel, handler_command_noti); // createConsumer(queue2, channel, handler2); @@ -561,6 +561,14 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { "positions.posExecutive", ], }); + + const oldPosMasters = await repoPosmaster.find({ + where: { + orgRevisionId: orgRevisionPublish!.id, + }, + select: ['id', 'current_holderId', 'ancestorDNA'] + }); + // Task #2160 ดึง posMasterAssign ของ revision เดิม const oldposMasterAssigns = await posMasterAssignRepository.find({ relations: ["posMaster"], @@ -613,9 +621,20 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { posMasterIdMap.set(pm.ancestorDNA?.trim() ?? '', pm.id); } + const oldPosMasterMap = new Map(); + for (const oldPm of oldPosMasters) { + const dna = oldPm.ancestorDNA?.trim(); + if (dna) { + oldPosMasterMap.set(dna, oldPm); + } + } + const _null: any = null; for (const item of posMaster) { + const dna = item.ancestorDNA?.trim(); + const oldPm = dna ? oldPosMasterMap.get(dna) : null; + // Task #2160 Clone posMasterAssign const assigns = assignMap.get(item.ancestorDNA); if (assigns && assigns.length > 0) { @@ -651,18 +670,26 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { await repoProfile.save(profile); } } - item.current_holderId = item.next_holderId; - // item.conditionReason = _null; - // if (item.current_holderId) { - // item.conditionReason = _null; - // item.isCondition = false; - // } - item.next_holderId = null; - item.lastUpdateUserId = lastUpdateUserId; - item.lastUpdateFullName = lastUpdateFullName; - item.lastUpdatedAt = lastUpdatedAt; - await repoPosmaster.save(item).catch((e) => console.log(e)); - await CreatePosMasterHistoryOfficer(item.id, null); + // item.current_holderId = item.next_holderId; + // item.next_holderId = null; + // item.lastUpdateUserId = lastUpdateUserId; + // item.lastUpdateFullName = lastUpdateFullName; + // item.lastUpdatedAt = lastUpdatedAt; + await repoPosmaster.update(item.id, { + current_holderId: item.next_holderId, + next_holderId: null, + lastUpdateUserId, + lastUpdateFullName, + lastUpdatedAt, + }); + + const oldHolderId = oldPm ? oldPm.current_holderId : null; + const newHolderId = item ? item.next_holderId : null; + const isHolderChanged = oldHolderId !== newHolderId; + + if (isHolderChanged) { + await CreatePosMasterHistoryOfficer(item.id, null); + } } for (const act of oldposMasterAct) { From 9507040f75a907ef0437e0309466083c3ded8a1b Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 2 Feb 2026 16:18:32 +0700 Subject: [PATCH 027/178] created: script active act position --- src/controllers/PosMasterActController.ts | 107 ++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 4d449d21..25595045 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -20,6 +20,7 @@ import { PosMaster } from "../entities/PosMaster"; import { Brackets, LessThan, MoreThan } from "typeorm"; import { OrgRevision } from "../entities/OrgRevision"; import Extension from "../interfaces/extension"; +import { ProfileActposition } from "../entities/ProfileActposition"; @Route("api/v1/org/pos/act") @Tags("PosMasterAct") @@ -32,6 +33,7 @@ export class PosMasterActController extends Controller { private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private posMasterActRepository = AppDataSource.getRepository(PosMasterAct); private posMasterRepository = AppDataSource.getRepository(PosMaster); + private actpositionRepository = AppDataSource.getRepository(ProfileActposition); /** * API เพิ่มรักษาการในตำแหน่ง @@ -535,4 +537,109 @@ export class PosMasterActController extends Controller { return new HttpSuccess(_posMaster); } + + /** + * API รักษาการในตำแหน่ง active โดยไม่ต้องออกคำสั่ง + * @summary รักษาการในตำแหน่ง active ในระบบโดยไม่ต้องออกคำสั่ง (SUPER ADMIN) + * @param {string} id Id หน่วยงาน + */ + @Post("{id}") + async activePosMasterAct(@Path() id: string, @Request() req: { user: Record }) { + const posMasterActs = await this.posMasterActRepository + .createQueryBuilder("posMasterAct") + .leftJoinAndSelect("posMasterAct.posMaster", "posMaster") + .leftJoinAndSelect("posMaster.orgRoot", "orgRoot") + .leftJoinAndSelect("posMaster.orgChild1", "orgChild1") + .leftJoinAndSelect("posMaster.orgChild2", "orgChild2") + .leftJoinAndSelect("posMaster.orgChild3", "orgChild3") + .leftJoinAndSelect("posMaster.orgChild4", "orgChild4") + .leftJoinAndSelect("posMaster.current_holder", "current_holder") + .leftJoinAndSelect("posMasterAct.posMasterChild", "posMasterChild") + .where("posMaster.orgRootId = :orgRootId", { orgRootId: id }) + .andWhere("posMasterAct.statusReport = :statusReport", { statusReport: "PENDING" }) + .select([ + "posMasterAct.id", + "posMasterAct.statusReport", + "posMaster.posMasterNo", + "orgRoot.orgRootShortName", + "orgChild1.orgChild1ShortName", + "orgChild2.orgChild2ShortName", + "orgChild3.orgChild3ShortName", + "orgChild4.orgChild4ShortName", + "current_holder.position", + "posMasterChild.current_holderId", + ]) + .getMany(); + + if (posMasterActs.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรักษาการในตำแหน่งของหน่วยงานนี้"); + } + + await Promise.all( + posMasterActs.map(async (posMasterAct) => { + const orgShortName = + [ + posMasterAct.posMaster?.orgChild4?.orgChild4ShortName, + posMasterAct.posMaster?.orgChild3?.orgChild3ShortName, + posMasterAct.posMaster?.orgChild2?.orgChild2ShortName, + posMasterAct.posMaster?.orgChild1?.orgChild1ShortName, + posMasterAct.posMaster?.orgRoot?.orgRootShortName, + ].find(Boolean) ?? ""; + + const profileId = posMasterAct.posMasterChild?.current_holderId; + + if (profileId) { + const existingActivePositions = await this.actpositionRepository.find({ + select: [ + "id", + "status", + "lastUpdateUserId", + "lastUpdateFullName", + "lastUpdatedAt", + "dateEnd", + ], + where: { profileId, status: true }, + }); + + if (existingActivePositions.length > 0) { + await Promise.all( + existingActivePositions.map(async (pos) => { + Object.assign(pos, { + status: false, + lastUpdateUserId: req.user?.sub ?? null, + lastUpdateFullName: req.user?.name ?? null, + lastUpdatedAt: new Date(), + dateEnd: new Date(), + }); + await this.actpositionRepository.save(pos); + }), + ); + } + } + + const dataAct = new ProfileActposition(); + Object.assign(dataAct, { + profileId: profileId ?? null, + dateStart: new Date(), + posNo: + orgShortName && posMasterAct.posMaster?.posMasterNo + ? `${orgShortName} ${posMasterAct.posMaster.posMasterNo}` + : posMasterAct.posMaster?.posMasterNo ?? "-", + position: posMasterAct.posMaster?.current_holder?.position ?? null, + posNoAbb: orgShortName, + status: true, + createdUserId: req.user?.sub ?? null, + createdFullName: req.user?.name ?? null, + lastUpdateUserId: req.user?.sub ?? null, + lastUpdateFullName: req.user?.name ?? null, + }); + await this.actpositionRepository.save(dataAct); + + posMasterAct.statusReport = "DONE"; + await this.posMasterActRepository.save(posMasterAct); + }), + ); + + return new HttpSuccess(); + } } From 0a3f0d9170c31cec2f8b91924284a1193278140a Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 2 Feb 2026 16:30:24 +0700 Subject: [PATCH 028/178] fix: return status of act position --- src/controllers/PosMasterActController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 25595045..061d014e 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -387,6 +387,7 @@ export class PosMasterActController extends Controller { posType: item.posMasterChild?.current_holder?.posType?.posTypeName ?? null, position: item.posMasterChild?.current_holder?.position ?? null, posNo: shortName, + statusReport: item.statusReport, }; }), ); From e5e407e122653b29810fac705e36fc850011f0d3 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 2 Feb 2026 17:50:48 +0700 Subject: [PATCH 029/178] fix: PUT /org/workflow/commander/operate isAct = true not response posExecutiveNameOrg --- src/controllers/WorkflowController.ts | 29 ++++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index 3bd6fe19..01b53dd3 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -150,16 +150,19 @@ export class WorkflowController extends Controller { if (body.sysName == "SYS_TRANSFER_REQ") { if (metaStateOp.operator == "PersonnelOfficer" && correspondingState?.order == 1) { return; - } - else if (metaStateOp.operator == "Officer" && [1, 2].includes(correspondingState?.order as number)) { - metaStateOp.operator = "PersonnelOfficer" + } else if ( + metaStateOp.operator == "Officer" && + [1, 2].includes(correspondingState?.order as number) + ) { + metaStateOp.operator = "PersonnelOfficer"; } } // Task #2208 กรณีขอแก้ไขข้อมูลทะเบียนประวัติ และ IDP และคนขออยู่ในสำนักปลัดกรุงเทพมหานคร - if (metaStateOp.operator == "Officer" && - (["REGISTRY_PROFILE", "REGISTRY_PROFILE_EMP", "REGISTRY_IDP"].includes(body.sysName)) + if ( + metaStateOp.operator == "Officer" && + ["REGISTRY_PROFILE", "REGISTRY_PROFILE_EMP", "REGISTRY_IDP"].includes(body.sysName) ) { - metaStateOp.operator = "PersonnelOfficer" + metaStateOp.operator = "PersonnelOfficer"; } } if (correspondingState) { @@ -1042,14 +1045,12 @@ export class WorkflowController extends Controller { ]); // 8. ปรับ response mapping (ถ้าจำเป็น) - const processedData = body.isAct - ? data - : data.map((x: any) => ({ - ...x, - posExecutiveNameOrg: - (x.posExecutiveName ?? "") + - (x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""), - })); + const processedData = data.map((x: any) => ({ + ...x, + posExecutiveNameOrg: + (x.posExecutiveName ?? "") + + (x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""), + })); return new HttpSuccess({ data: processedData, total }); } From ec04da56118cc3c90f6769e988a1d530b52c78c1 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 2 Feb 2026 18:33:30 +0700 Subject: [PATCH 030/178] migrate create table commandOperator #2220 --- src/entities/Command.ts | 4 + src/entities/CommandOperator.ts | 97 +++++++++++++++++++ ...0023808315-create_table_commandOperator.ts | 17 ++++ 3 files changed, 118 insertions(+) create mode 100644 src/entities/CommandOperator.ts create mode 100644 src/migration/1770023808315-create_table_commandOperator.ts diff --git a/src/entities/Command.ts b/src/entities/Command.ts index 029f4cdc..c6b26626 100644 --- a/src/entities/Command.ts +++ b/src/entities/Command.ts @@ -4,6 +4,7 @@ import { CommandType } from "./CommandType"; import { CommandSend } from "./CommandSend"; import { CommandSalary } from "./CommandSalary"; import { CommandRecive } from "./CommandRecive"; +import { CommandOperator } from "./CommandOperator"; import { ProfileSalary } from "./ProfileSalary"; import { ProfileSalaryHistory } from "./ProfileSalaryHistory"; import { CommandSign } from "./CommandSign"; @@ -165,6 +166,9 @@ export class Command extends EntityBase { @OneToMany(() => CommandRecive, (commandRecive) => commandRecive.command) commandRecives: CommandRecive[]; + @OneToMany(() => CommandOperator, (commandOperator) => commandOperator.command) + commandOperators: CommandOperator[]; + @OneToMany(() => ProfileSalary, (profileSalary) => profileSalary.command) profileSalarys: ProfileSalary[]; diff --git a/src/entities/CommandOperator.ts b/src/entities/CommandOperator.ts new file mode 100644 index 00000000..02b452b2 --- /dev/null +++ b/src/entities/CommandOperator.ts @@ -0,0 +1,97 @@ +import { Entity, Column, JoinColumn, ManyToOne, Double } from "typeorm"; +import { EntityBase } from "./base/Base"; +import { Command } from "./Command"; + +@Entity("commandOperator") +export class CommandOperator extends EntityBase { + @Column({ + nullable: true, + comment: "คีย์นอก(FK)ของตาราง profile", + length: 40, + default: null, + }) + profileId: string; + + @Column({ + nullable: true, + comment: "คำนำหน้า", + length: 255, + default: null, + }) + prefix: string; + + @Column({ + nullable: true, + comment: "ชื่อ", + length: 255, + default: null, + }) + firstName: string; + + @Column({ + nullable: true, + comment: "สกุล", + length: 255, + default: null, + }) + lastName: string; + + @Column({ + nullable: true, + comment: "เลขที่ตำแหน่ง", + length: 255, + default: null, + }) + posNo: string; + + @Column({ + nullable: true, + comment: "ประเภท", + length: 255, + default: null, + }) + posType: string; + + @Column({ + nullable: true, + comment: "ระดับ", + length: 255, + default: null, + }) + posLevel: string; + + @Column({ + nullable: true, + comment: "ตำแหน่งในสายงาน", + length: 255, + default: null, + }) + position: string; + + @Column({ + nullable: true, + comment: "ตำแหน่งทางการบริหาร", + length: 255, + default: null, + }) + positionExecutive: string; + + @Column({ + nullable: true, + comment: "บทบาทของเจ้าหน้าที่ดำเนินการ เช่น ผอ.สกจ. / รักษาการ ผอ.สกจ. / ผอ. กบห. / รักษาการ ผอ. กบห. / ผอ. ส่วน", + length: 255, + default: null, + }) + roleName: string; + + @Column({ + length: 40, + comment: "คีย์นอก(FK)ของตาราง command", + }) + commandId: string; + + @ManyToOne(() => Command, (command) => command.commandOperators) + @JoinColumn({ name: "commandId" }) + command: Command; + +} diff --git a/src/migration/1770023808315-create_table_commandOperator.ts b/src/migration/1770023808315-create_table_commandOperator.ts new file mode 100644 index 00000000..36057adf --- /dev/null +++ b/src/migration/1770023808315-create_table_commandOperator.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateTableCommandOperator1770023808315 implements MigrationInterface { + name = 'CreateTableCommandOperator1770023808315' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE \`commandOperator\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง profile', \`prefix\` varchar(255) NULL COMMENT 'คำนำหน้า', \`firstName\` varchar(255) NULL COMMENT 'ชื่อ', \`lastName\` varchar(255) NULL COMMENT 'สกุล', \`posNo\` varchar(255) NULL COMMENT 'เลขที่ตำแหน่ง', \`posType\` varchar(255) NULL COMMENT 'ประเภท', \`posLevel\` varchar(255) NULL COMMENT 'ระดับ', \`position\` varchar(255) NULL COMMENT 'ตำแหน่งในสายงาน', \`positionExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร', \`roleName\` varchar(255) NULL COMMENT 'บทบาทของเจ้าหน้าที่ดำเนินการ เช่น ผอ.สกจ. / รักษาการ ผอ.สกจ. / ผอ. กบห. / รักษาการ ผอ. กบห. / ผอ. ส่วน', \`commandId\` varchar(40) NOT NULL COMMENT 'คีย์นอก(FK)ของตาราง command', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`ALTER TABLE \`commandOperator\` ADD CONSTRAINT \`FK_343a2ecd7cb855397f19a990008\` FOREIGN KEY (\`commandId\`) REFERENCES \`command\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`commandOperator\` DROP FOREIGN KEY \`FK_343a2ecd7cb855397f19a990008\``); + await queryRunner.query(`DROP TABLE \`commandOperator\``); + } + +} From 4ec334f0d434fb119c3f6171aef6f7ea30243eae Mon Sep 17 00:00:00 2001 From: Adisak Date: Tue, 3 Feb 2026 10:27:48 +0700 Subject: [PATCH 031/178] fix: cronjob publish fail --- src/app.ts | 2 +- src/services/rabbitmq.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index 578a8e65..75d0bfea 100644 --- a/src/app.ts +++ b/src/app.ts @@ -52,7 +52,7 @@ async function main() { const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_PORT = +(process.env.APP_PORT || 3000); - const cronTime = "0 0 3 * * *"; // ตั้งเวลาทุกวันเวลา 08:00:00 + const cronTime = "0 0 3 * * *"; // ตั้งเวลาทุกวันเวลา 03:00:00 // const cronTime = "*/10 * * * * *"; cron.schedule(cronTime, async () => { try { diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 8e9f11f4..784f3873 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -708,11 +708,11 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { posMasterId: newParentId, posMasterChildId: newChildId, createdAt: new Date(), - createdFullName: user.name, - createdUserId: user.sub, + createdFullName: user ? user.name : "system", + createdUserId: user ? user.sub : "system", lastUpdatedAt: new Date(), - lastUpdateFullName: user.name, - lastUpdateUserId: user.sub, + lastUpdateFullName: user ? user.name : "system", + lastUpdateUserId: user ? user.sub : "system", }; await posMasterActRepository.save(newAct); From bb18fed9aebab6bd609aae52d075127012fe585c Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 3 Feb 2026 12:22:55 +0700 Subject: [PATCH 032/178] =?UTF-8?q?migrate=20add=20commandOperator.orderNo?= =?UTF-8?q?=20&=20api=20=E0=B8=AA=E0=B9=88=E0=B8=A7=E0=B8=99=E0=B9=80?= =?UTF-8?q?=E0=B8=88=E0=B9=89=E0=B8=B2=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=94=E0=B8=B3=E0=B9=80=E0=B8=99?= =?UTF-8?q?=E0=B8=B4=E0=B8=99=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=97=E0=B8=B5?= =?UTF-8?q?=E0=B9=88=E0=B8=84=E0=B8=B3=E0=B8=AA=E0=B8=B1=E0=B9=88=E0=B8=87?= =?UTF-8?q?=20=20#2220?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 133 ++++++++--- src/controllers/CommandOperatorController.ts | 224 ++++++++++++++++++ src/entities/CommandOperator.ts | 20 ++ ...able_commandOperator_add_column_orderNo.ts | 14 ++ 4 files changed, 363 insertions(+), 28 deletions(-) create mode 100644 src/controllers/CommandOperatorController.ts create mode 100644 src/migration/1770089334925-update_table_commandOperator_add_column_orderNo.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 905c77ea..d31bfa65 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -27,6 +27,7 @@ import { OrgRevision } from "../entities/OrgRevision"; import { CommandSendCC } from "../entities/CommandSendCC"; import { CommandSalary } from "../entities/CommandSalary"; import { CommandRecive } from "../entities/CommandRecive"; +import { CommandOperator } from "../entities/CommandOperator"; import HttpStatus from "../interfaces/http-status"; import Extension from "../interfaces/extension"; import { ProfileEmployee } from "../entities/ProfileEmployee"; @@ -112,6 +113,7 @@ export class CommandController extends Controller { private commandSendCCRepository = AppDataSource.getRepository(CommandSendCC); private commandSalaryRepository = AppDataSource.getRepository(CommandSalary); private commandReciveRepository = AppDataSource.getRepository(CommandRecive); + private commandOperatorRepository = AppDataSource.getRepository(CommandOperator); private profileRepository = AppDataSource.getRepository(Profile); private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); @@ -2408,6 +2410,7 @@ export class CommandController extends Controller { }, @Request() request: RequestWithUser, ) { + const now = new Date(); let command = new Command(); let commandCode: string = ""; let _null: any = null; @@ -2466,12 +2469,86 @@ export class CommandController extends Controller { : _null), (command.createdUserId = request.user.sub); command.createdFullName = request.user.name; - command.createdAt = new Date(); + command.createdAt = now; command.lastUpdateUserId = request.user.sub; command.lastUpdateFullName = request.user.name; - command.lastUpdatedAt = new Date(); + command.lastUpdatedAt = now; await this.commandRepository.save(command); } + // insert commandOperator + if (request.user.sub) { + const profile = await this.profileRepository.findOne({ + where: { keycloak: request.user.sub }, + relations: { + posLevel: true, + posType: true, + current_holders: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }, + }); + if (profile) { + const currentHolder = profile!.current_holders?.find( + x => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + const posNo = + currentHolder != null && currentHolder.orgChild4 != null + ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild3 != null + ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild2 != null + ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild1 != null + ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder?.orgRoot != null + ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` + : null; + + const position = await this.positionRepository.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: currentHolder?.orgRevisionId, + current_holderId: profile!.id, + }, + }, + order: { createdAt: "DESC" }, + relations: { posExecutive: true }, + }); + const operator = Object.assign( + new CommandOperator(), + { + profileId: profile?.id, + prefix: profile?.prefix, + firstName: profile?.firstName, + lastName: profile?.lastName, + posNo: posNo, + posType: profile?.posType?.posTypeName ?? null, + posLevel: profile?.posLevel?.posLevelName ?? null, + position: position?.positionName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + roleName: "เจ้าหน้าที่ดำเนินการ", + orderNo: 1, + commandId: command.id, + createUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + } + ); + await this.commandOperatorRepository.save(operator); + } + } const path = commandTypePath(commandCode); if (path == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ"); @@ -2537,10 +2614,10 @@ export class CommandController extends Controller { commandRecive.commandId = command.id; commandRecive.createdUserId = request.user.sub; commandRecive.createdFullName = request.user.name; - commandRecive.createdAt = new Date(); + commandRecive.createdAt = now; commandRecive.lastUpdateUserId = request.user.sub; commandRecive.lastUpdateFullName = request.user.name; - commandRecive.lastUpdatedAt = new Date(); + commandRecive.lastUpdatedAt = now; if (commandCode == "C-PM-40") { const posMasterAct = await this.posMasterActRepository.findOne({ @@ -2635,10 +2712,10 @@ export class CommandController extends Controller { commandSend.commandId = command.id; commandSend.createdUserId = request.user.sub; commandSend.createdFullName = request.user.name; - commandSend.createdAt = new Date(); + commandSend.createdAt = now; commandSend.lastUpdateUserId = request.user.sub; commandSend.lastUpdateFullName = request.user.name; - commandSend.lastUpdatedAt = new Date(); + commandSend.lastUpdatedAt = now; await this.commandSendRepository.save(commandSend); if (commandSend && commandSend.id) { let _ccName = new Array("EMAIL", "INBOX"); @@ -2649,10 +2726,10 @@ export class CommandController extends Controller { name: _ccName[i], createdUserId: request.user.sub, createdFullName: request.user.name, - createdAt: new Date(), + createdAt: now, lastUpdateUserId: request.user.sub, lastUpdateFullName: request.user.name, - lastUpdatedAt: new Date(), + lastUpdatedAt: now, }); } await this.commandSendCCRepository.save(_dataSendCC); @@ -2692,10 +2769,10 @@ export class CommandController extends Controller { commandSend.commandId = command.id; commandSend.createdUserId = request.user.sub; commandSend.createdFullName = request.user.name; - commandSend.createdAt = new Date(); + commandSend.createdAt = now; commandSend.lastUpdateUserId = request.user.sub; commandSend.lastUpdateFullName = request.user.name; - commandSend.lastUpdatedAt = new Date(); + commandSend.lastUpdatedAt = now; await this.commandSendRepository.save(commandSend); if (commandSend && commandSend.id) { let _ccName = new Array("EMAIL", "INBOX"); @@ -2706,10 +2783,10 @@ export class CommandController extends Controller { name: _ccName[i], createdUserId: request.user.sub, createdFullName: request.user.name, - createdAt: new Date(), + createdAt: now, lastUpdateUserId: request.user.sub, lastUpdateFullName: request.user.name, - lastUpdatedAt: new Date(), + lastUpdatedAt: now, }); } await this.commandSendCCRepository.save(_dataSendCC); @@ -2749,10 +2826,10 @@ export class CommandController extends Controller { commandSend.commandId = command.id; commandSend.createdUserId = request.user.sub; commandSend.createdFullName = request.user.name; - commandSend.createdAt = new Date(); + commandSend.createdAt = now; commandSend.lastUpdateUserId = request.user.sub; commandSend.lastUpdateFullName = request.user.name; - commandSend.lastUpdatedAt = new Date(); + commandSend.lastUpdatedAt = now; await this.commandSendRepository.save(commandSend); if (commandSend && commandSend.id) { let _ccName = new Array("EMAIL", "INBOX"); @@ -2763,10 +2840,10 @@ export class CommandController extends Controller { name: _ccName[i], createdUserId: request.user.sub, createdFullName: request.user.name, - createdAt: new Date(), + createdAt: now, lastUpdateUserId: request.user.sub, lastUpdateFullName: request.user.name, - lastUpdatedAt: new Date(), + lastUpdatedAt: now, }); } await this.commandSendCCRepository.save(_dataSendCC); @@ -2931,10 +3008,10 @@ export class CommandController extends Controller { commandSend.commandId = command.id; commandSend.createdUserId = request.user.sub; commandSend.createdFullName = request.user.name; - commandSend.createdAt = new Date(); + commandSend.createdAt = now; commandSend.lastUpdateUserId = request.user.sub; commandSend.lastUpdateFullName = request.user.name; - commandSend.lastUpdatedAt = new Date(); + commandSend.lastUpdatedAt = now; await this.commandSendRepository.save(commandSend); if (commandSend && commandSend.id) { let _ccName = new Array("EMAIL", "INBOX"); @@ -2945,10 +3022,10 @@ export class CommandController extends Controller { name: _ccName[i], createdUserId: request.user.sub, createdFullName: request.user.name, - createdAt: new Date(), + createdAt: now, lastUpdateUserId: request.user.sub, lastUpdateFullName: request.user.name, - lastUpdatedAt: new Date(), + lastUpdatedAt: now, }); } await this.commandSendCCRepository.save(_dataSendCC); @@ -2992,10 +3069,10 @@ export class CommandController extends Controller { commandSend.commandId = command.id; commandSend.createdUserId = request.user.sub; commandSend.createdFullName = request.user.name; - commandSend.createdAt = new Date(); + commandSend.createdAt = now; commandSend.lastUpdateUserId = request.user.sub; commandSend.lastUpdateFullName = request.user.name; - commandSend.lastUpdatedAt = new Date(); + commandSend.lastUpdatedAt = now; await this.commandSendRepository.save(commandSend); if (commandSend && commandSend.id) { let _ccName = new Array("EMAIL", "INBOX"); @@ -3006,10 +3083,10 @@ export class CommandController extends Controller { name: _ccName[i], createdUserId: request.user.sub, createdFullName: request.user.name, - createdAt: new Date(), + createdAt: now, lastUpdateUserId: request.user.sub, lastUpdateFullName: request.user.name, - lastUpdatedAt: new Date(), + lastUpdatedAt: now, }); } await this.commandSendCCRepository.save(_dataSendCC); @@ -3051,10 +3128,10 @@ export class CommandController extends Controller { commandSend.commandId = command.id; commandSend.createdUserId = request.user.sub; commandSend.createdFullName = request.user.name; - commandSend.createdAt = new Date(); + commandSend.createdAt = now; commandSend.lastUpdateUserId = request.user.sub; commandSend.lastUpdateFullName = request.user.name; - commandSend.lastUpdatedAt = new Date(); + commandSend.lastUpdatedAt = now; await this.commandSendRepository.save(commandSend); if (commandSend && commandSend.id) { let _ccName = new Array("EMAIL", "INBOX"); @@ -3065,10 +3142,10 @@ export class CommandController extends Controller { name: _ccName[i], createdUserId: request.user.sub, createdFullName: request.user.name, - createdAt: new Date(), + createdAt: now, lastUpdateUserId: request.user.sub, lastUpdateFullName: request.user.name, - lastUpdatedAt: new Date(), + lastUpdatedAt: now, }); } await this.commandSendCCRepository.save(_dataSendCC); diff --git a/src/controllers/CommandOperatorController.ts b/src/controllers/CommandOperatorController.ts new file mode 100644 index 00000000..f1cac92a --- /dev/null +++ b/src/controllers/CommandOperatorController.ts @@ -0,0 +1,224 @@ +import { + Controller, + Post, + Delete, + Route, + Security, + Tags, + Body, + Path, + Request, + Response, + Get +} from "tsoa"; +import { LessThan, MoreThan } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatusCode from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { Command } from "../entities/Command"; +import { CommandOperator, CreateCommandOperatorDto } from "../entities/CommandOperator"; +import { RequestWithUser } from "../middlewares/user"; + +@Route("api/v1/org/commandOperator") +@Tags("CommandOperator") +@Security("bearerAuth") +@Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการได้ กรุณาลองใหม่ในภายหลัง", +) +export class CommandOperatorController extends Controller { + private commandRepo = AppDataSource.getRepository(Command); + private commandOperatorRepo = AppDataSource.getRepository(CommandOperator); + + /** + * API รายชื่อเจ้าหน้าที่ดำเนินการที่คำสั่ง + * @summary API รายชื่อเจ้าหน้าที่ดำเนินการที่คำสั่ง + * @param commandId คีย์คำสั่ง + */ + @Get("{commandId}") + async getCommandOperatorByCommandId( + @Path() commandId: string + ) { + const command = await this.commandRepo.findOne({ + where: { id: commandId }, + select: { id: true }, + }); + if (!command) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลคำสั่งนี้"); + } + const commandOperators = await this.commandOperatorRepo.find({ + where: { commandId: command.id }, + order: { orderNo: "ASC" }, + }); + return new HttpSuccess(commandOperators); + } + + /** + * API สลับลำดับเจ้าหน้าที่ดำเนินการ (UP / DOWN) + * @summary API สลับลำดับเจ้าหน้าที่ดำเนินการ (UP / DOWN) + * @param direction สลับขึ้นหรือลง (UP / DOWN) + * @param operatorId คีย์เจ้าหน้าที่ดำเนินการ + */ + @Get("swap/{direction}/{operatorId}") + async swapCommandOperator( + @Path() direction: string, + @Path() operatorId: string, + ) { + const source = await this.commandOperatorRepo.findOne({ + where: { id: operatorId }, + }); + + if (!source) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลเจ้าหน้าที่"); + } + + const sourceOrder = source.orderNo; + const isUp = direction.trim().toUpperCase() === "UP"; + + let dest: CommandOperator | null; + + if (isUp) { + dest = await this.commandOperatorRepo.findOne({ + where: { + commandId: source.commandId, + orderNo: LessThan(sourceOrder), + }, + order: { orderNo: "DESC" }, + }); + } else { + dest = await this.commandOperatorRepo.findOne({ + where: { + commandId: source.commandId, + orderNo: MoreThan(sourceOrder), + }, + order: { orderNo: "ASC" }, + }); + } + + // ถ้าไม่มีตัวให้สลับ (บนสุด / ล่างสุด) + if (!dest) { + return new HttpSuccess(); + } + + // swap + const temp = source.orderNo; + source.orderNo = dest.orderNo; + dest.orderNo = temp; + + await Promise.all([ + this.commandOperatorRepo.save(source), + this.commandOperatorRepo.save(dest), + ]); + + return new HttpSuccess(); + } + + /** + * API เพิ่มเจ้าหน้าที่ดำเนินการที่คำสั่ง + * @summary API เพิ่มเจ้าหน้าที่ดำเนินการที่คำสั่ง + * @param commandId คีย์คำสั่ง + */ + @Post("{commandId}") + async createCommandOperators( + @Path() commandId: string, + @Request() request: RequestWithUser, + @Body() body: CreateCommandOperatorDto, + ) { + const command = await this.commandRepo.findOne({ + where: { id: commandId }, + select: { id: true }, + }); + + if (!command) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลคำสั่งนี้"); + } + const lastOrderNo = await this.commandOperatorRepo.findOne({ + where: { commandId: commandId }, + order: { orderNo: "DESC" }, + select: { orderNo: true }, + }); + const nextOrderNo = (lastOrderNo?.orderNo ?? 1) + 1; + + const now = new Date(); + const operator = Object.assign( + new CommandOperator(), + { + ...body, + commandId: command.id, + orderNo: nextOrderNo, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + } + ); + await this.commandOperatorRepo.save(operator); + return new HttpSuccess(); + } + + /** + * API ลบเจ้าหน้าที่ดำเนินการที่คำสั่ง + * @summary API ลบเจ้าหน้าที่ดำเนินการที่คำสั่ง + * @param commandId คีย์คำสั่ง + * @param operatorId คีย์เจ้าหน้าที่ดำเนินการ + */ + @Delete("{commandId}/{operatorId}") + public async deleteCommandOperator( + @Path() commandId: string, + @Path() operatorId: string, + ) { + const queryRunner = AppDataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 1. หา operator + const operator = await queryRunner.manager.findOne(CommandOperator, { + where: { + id: operatorId, + commandId: commandId, + }, + }); + + if (!operator) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบเจ้าหน้าที่ดำเนินการ"); + } + + // 2. ห้ามลบ orderNo = 1 + if (operator.orderNo === 1) { + throw new HttpError( + HttpStatusCode.BAD_REQUEST, + "ไม่สามารถลบเจ้าหน้าที่ลำดับที่ 1 ได้" + ); + } + + const removedOrderNo = operator.orderNo; + + // 3. ลบ + await queryRunner.manager.remove(operator); + + // 4. re orderNumber ตัวที่เหลือ + await queryRunner.manager + .createQueryBuilder() + .update(CommandOperator) + .set({ + orderNo: () => "orderNo - 1", + }) + .where("commandId = :commandId", { commandId }) + .andWhere("orderNo > :removedOrderNo", { removedOrderNo }) + .execute(); + + await queryRunner.commitTransaction(); + return new HttpSuccess(true); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + +} diff --git a/src/entities/CommandOperator.ts b/src/entities/CommandOperator.ts index 02b452b2..f2d42a6b 100644 --- a/src/entities/CommandOperator.ts +++ b/src/entities/CommandOperator.ts @@ -84,6 +84,13 @@ export class CommandOperator extends EntityBase { }) roleName: string; + @Column({ + nullable: true, + comment: "ลำดับบทบาทของเจ้าหน้าที่ดำเนินการ", + default: null, + }) + orderNo: number; + @Column({ length: 40, comment: "คีย์นอก(FK)ของตาราง command", @@ -95,3 +102,16 @@ export class CommandOperator extends EntityBase { command: Command; } + +export class CreateCommandOperatorDto { + profileId: string; + prefix?: string; + firstName?: string; + lastName?: string; + posNo?: string; + posType?: string; + posLevel?: string; + position?: string; + positionExecutive?: string; + roleName: string; +} \ No newline at end of file diff --git a/src/migration/1770089334925-update_table_commandOperator_add_column_orderNo.ts b/src/migration/1770089334925-update_table_commandOperator_add_column_orderNo.ts new file mode 100644 index 00000000..11990b5b --- /dev/null +++ b/src/migration/1770089334925-update_table_commandOperator_add_column_orderNo.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateTableCommandOperatorAddColumnOrderNo1770089334925 implements MigrationInterface { + name = 'UpdateTableCommandOperatorAddColumnOrderNo1770089334925' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`commandOperator\` ADD \`orderNo\` int NULL COMMENT 'ลำดับบทบาทของเจ้าหน้าที่ดำเนินการ'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`commandOperator\` DROP COLUMN \`orderNo\``); + } + +} From 30bf5ad9e37ec6e932740eba3b961319418afeeb Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 3 Feb 2026 17:44:30 +0700 Subject: [PATCH 033/178] =?UTF-8?q?migrate=20add=20column=20isDeleted=20+?= =?UTF-8?q?=20API=20=E0=B8=A5=E0=B8=9A=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1?= =?UTF-8?q?=E0=B8=B9=E0=B8=A5=E0=B8=9D=E0=B8=B6=E0=B8=81=E0=B8=AD=E0=B8=9A?= =?UTF-8?q?=E0=B8=A3=E0=B8=A1/=E0=B8=94=E0=B8=B9=E0=B8=87=E0=B8=B2?= =?UTF-8?q?=E0=B8=99=20+=20=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=9E=E0=B8=B1?= =?UTF-8?q?=E0=B8=92=E0=B8=99=E0=B8=B2=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=9A?= =?UTF-8?q?=E0=B8=B8=E0=B8=84=E0=B8=84=E0=B8=A5=20idp=20+=20=E0=B8=A3?= =?UTF-8?q?=E0=B8=B1=E0=B8=81=E0=B8=A9=E0=B8=B2=E0=B8=81=E0=B8=B2=E0=B8=A3?= =?UTF-8?q?=20=20Task=20#2276,=20#2279,=20#2278?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 81 +++++++++++++++++- .../ProfileActpositionController.ts | 42 +++++++++- .../ProfileDevelopmentController.ts | 43 +++++++++- src/controllers/ProfileTrainingController.ts | 82 ++++++++++++++++++- src/entities/ProfileActposition.ts | 7 ++ src/entities/ProfileActpositionHistory.ts | 7 ++ src/entities/ProfileDevelopment.ts | 7 ++ src/entities/ProfileDevelopmentHistory.ts | 7 ++ src/entities/ProfileTraining.ts | 7 ++ src/entities/ProfileTrainingHistory.ts | 7 ++ ...opment_actPosition_add_column_isDeleted.ts | 18 ++++ ...opment_actPosition_add_column_isDeleted.ts | 18 ++++ 12 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 src/migration/1770110880489-update_tables_tranning_development_actPosition_add_column_isDeleted.ts create mode 100644 src/migration/1770112472041-update_tables_history_tranning_development_actPosition_add_column_isDeleted.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index d31bfa65..45bfa186 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -392,6 +392,7 @@ export class CommandController extends Controller { if (!commandType) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ"); } + const now = new Date(); command.detailHeader = commandType.detailHeader; command.detailBody = commandType.detailBody; command.detailFooter = commandType.detailFooter; @@ -401,11 +402,85 @@ export class CommandController extends Controller { command.issue = commandType.name; command.createdUserId = request.user.sub; command.createdFullName = request.user.name; - command.createdAt = new Date(); + command.createdAt = now; command.lastUpdateUserId = request.user.sub; command.lastUpdateFullName = request.user.name; - command.lastUpdatedAt = new Date(); + command.lastUpdatedAt = now; await this.commandRepository.save(command); + // insert commandOperator + if (request.user.sub) { + const profile = await this.profileRepository.findOne({ + where: { keycloak: request.user.sub }, + relations: { + posLevel: true, + posType: true, + current_holders: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }, + }); + if (profile) { + const currentHolder = profile!.current_holders?.find( + x => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + const posNo = + currentHolder != null && currentHolder.orgChild4 != null + ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild3 != null + ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild2 != null + ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild1 != null + ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder?.orgRoot != null + ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` + : null; + + const position = await this.positionRepository.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: currentHolder?.orgRevisionId, + current_holderId: profile!.id, + }, + }, + order: { createdAt: "DESC" }, + relations: { posExecutive: true }, + }); + const operator = Object.assign( + new CommandOperator(), + { + profileId: profile?.id, + prefix: profile?.prefix, + firstName: profile?.firstName, + lastName: profile?.lastName, + posNo: posNo, + posType: profile?.posType?.posTypeName ?? null, + posLevel: profile?.posLevel?.posLevelName ?? null, + position: position?.positionName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + roleName: "เจ้าหน้าที่ดำเนินการ", + orderNo: 1, + commandId: command.id, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + } + ); + await this.commandOperatorRepository.save(operator); + } + } return new HttpSuccess(command.id); } @@ -2538,7 +2613,7 @@ export class CommandController extends Controller { roleName: "เจ้าหน้าที่ดำเนินการ", orderNo: 1, commandId: command.id, - createUserId: request.user.sub, + createdUserId: request.user.sub, createdFullName: request.user.name, createdAt: now, lastUpdateUserId: request.user.sub, diff --git a/src/controllers/ProfileActpositionController.ts b/src/controllers/ProfileActpositionController.ts index 8e1aa9ff..e22cf983 100644 --- a/src/controllers/ProfileActpositionController.ts +++ b/src/controllers/ProfileActpositionController.ts @@ -40,7 +40,7 @@ export class ProfileActpositionController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileActpositionId = await this.profileActpositionRepo.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileActpositionId) { @@ -58,7 +58,7 @@ export class ProfileActpositionController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const getProfileActpositionId = await this.profileActpositionRepo.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileActpositionId) { @@ -201,6 +201,44 @@ export class ProfileActpositionController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลรักษาการในตำแหน่ง + * @summary API ลบข้อมูลรักษาการในตำแหน่ง + * @param actpositionId คีย์รักษาการในตำแหน่ง + */ + @Patch("update-delete/{actpositionId}") + public async updateIsDeletedTraining( + @Request() req: RequestWithUser, + @Path() actpositionId: string, + ) { + const record = await this.profileActpositionRepo.findOneBy({ id: actpositionId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileActpositionHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileActpositionRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileActpositionHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{actpositionId}") public async deleteProfileActposition( @Path() actpositionId: string, diff --git a/src/controllers/ProfileDevelopmentController.ts b/src/controllers/ProfileDevelopmentController.ts index 3a615a5a..3557e760 100644 --- a/src/controllers/ProfileDevelopmentController.ts +++ b/src/controllers/ProfileDevelopmentController.ts @@ -28,6 +28,7 @@ import permission from "../interfaces/permission"; import { DevelopmentProject } from "../entities/DevelopmentProject"; import { In, Brackets } from "typeorm"; import { DevelopmentRequest } from "../entities/DevelopmentRequest"; +import { setLogDataDiff } from "../interfaces/utils"; @Route("api/v1/org/profile/development") @Tags("ProfileDevelopment") @Security("bearerAuth") @@ -45,7 +46,7 @@ export class ProfileDevelopmentController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.developmentRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -66,7 +67,7 @@ export class ProfileDevelopmentController extends Controller { await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); let query = await AppDataSource.getRepository(ProfileDevelopment) .createQueryBuilder("profileDevelopment") - .where({ profileId: profileId }) + .where({ profileId: profileId, isDeleted: false }) .andWhere( new Brackets((qb) => { qb.where( @@ -329,6 +330,44 @@ export class ProfileDevelopmentController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการพัฒนารายบุคคล IDP + * @summary API ลบข้อมูลการพัฒนารายบุคคล IDP + * @param developmentId คีย์การพัฒนารายบุคคล IDP + */ + @Patch("update-delete/{developmentId}") + public async updateIsDeletedTraining( + @Request() req: RequestWithUser, + @Path() developmentId: string, + ) { + const record = await this.developmentRepository.findOneBy({ id: developmentId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileDevelopmentHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.developmentRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.developmentHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{developmentId}") public async deleteDevelopment(@Path() developmentId: string, @Request() req: RequestWithUser) { const _record = await this.developmentRepository.findOneBy({ id: developmentId }); diff --git a/src/controllers/ProfileTrainingController.ts b/src/controllers/ProfileTrainingController.ts index c331fdb9..0df7594f 100644 --- a/src/controllers/ProfileTrainingController.ts +++ b/src/controllers/ProfileTrainingController.ts @@ -25,6 +25,9 @@ import { RequestWithUser } from "../middlewares/user"; import { Profile } from "../entities/Profile"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { ProfileDevelopment } from "../entities/ProfileDevelopment"; +import { ProfileDevelopmentHistory } from "../entities/ProfileDevelopmentHistory"; +import { In } from "typeorm"; @Route("api/v1/org/profile/training") @Tags("ProfileTraining") @Security("bearerAuth") @@ -32,6 +35,8 @@ export class ProfileTrainingController extends Controller { private profileRepo = AppDataSource.getRepository(Profile); private trainingRepo = AppDataSource.getRepository(ProfileTraining); private trainingHistoryRepo = AppDataSource.getRepository(ProfileTrainingHistory); + private developmentRepo = AppDataSource.getRepository(ProfileDevelopment); + private developmentHistoryRepo = AppDataSource.getRepository(ProfileDevelopmentHistory); @Get("user") public async getTrainingUser(@Request() request: { user: Record }) { @@ -40,7 +45,7 @@ export class ProfileTrainingController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.trainingRepo.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -52,7 +57,7 @@ export class ProfileTrainingController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const record = await this.trainingRepo.find({ - where: { profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -178,4 +183,77 @@ export class ProfileTrainingController extends Controller { return new HttpSuccess(); } + + /** + * API ลบข้อมูลการฝึกอบรม/ดูงาน + * @summary API ลบข้อมูลการฝึกอบรม/ดูงาน + * @param trainingId คีย์การฝึกอบรม/ดูงาน + */ + @Patch("update-delete/{trainingId}") + public async updateIsDeletedTraining( + @Request() req: RequestWithUser, + @Path() trainingId: string, + ) { + const record = await this.trainingRepo.findOneBy({ id: trainingId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileTrainingHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileTrainingId = trainingId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.trainingRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.trainingHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + + /** + * API ล้างข้อมูลการฝึกอบรม/ดูงาน และ การพัฒนารายบุคคล IDP เมื่อลบโครงการพัฒนา + * @summary API ล้างข้อมูลการฝึกอบรม/ดูงาน และ การพัฒนารายบุคคล IDP เมื่อลบโครงการพัฒนา + */ + @Post("delete-all") + public async deleteAllTraining( + @Body() reqBody: { developmentId: string }, + @Request() req: RequestWithUser + ) { + const trainings = await this.trainingRepo.find({ + select: { id: true }, + where: { developmentId: reqBody.developmentId }, + }); + if (trainings.length > 0) { + const trainingIds = trainings.map((x) => x.id); + await this.trainingHistoryRepo.delete({ + profileTrainingId: In(trainingIds), + }); + await this.trainingRepo.delete({ + developmentId: reqBody.developmentId, + }); + } + + await this.developmentHistoryRepo.delete({ + kpiDevelopmentId: reqBody.developmentId, + }); + await this.developmentRepo.delete({ + kpiDevelopmentId: reqBody.developmentId + }); + + return new HttpSuccess(); + } + } diff --git a/src/entities/ProfileActposition.ts b/src/entities/ProfileActposition.ts index bdfd2391..4d20c0d8 100644 --- a/src/entities/ProfileActposition.ts +++ b/src/entities/ProfileActposition.ts @@ -86,6 +86,13 @@ export class ProfileActposition extends EntityBase { }) profileEmployeeId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูลรักษาการในตำแหน่ง", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileActpositionHistory, (profileActpositionHistory) => profileActpositionHistory.histories, diff --git a/src/entities/ProfileActpositionHistory.ts b/src/entities/ProfileActpositionHistory.ts index bb2034e7..723831d0 100644 --- a/src/entities/ProfileActpositionHistory.ts +++ b/src/entities/ProfileActpositionHistory.ts @@ -48,6 +48,13 @@ export class ProfileActpositionHistory extends EntityBase { }) profileActpositionId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูลรักษาการในตำแหน่ง", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileActposition, (profileActposition) => profileActposition.profileActpositionHistorys, diff --git a/src/entities/ProfileDevelopment.ts b/src/entities/ProfileDevelopment.ts index 66a79b50..33a1fa11 100644 --- a/src/entities/ProfileDevelopment.ts +++ b/src/entities/ProfileDevelopment.ts @@ -154,6 +154,13 @@ export class ProfileDevelopment extends EntityBase { }) kpiDevelopmentId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูลการพัฒนารายบุคคล (Individual Development Plan)", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileDevelopmentHistory, (profileDevelopmentHistory) => profileDevelopmentHistory.histories, diff --git a/src/entities/ProfileDevelopmentHistory.ts b/src/entities/ProfileDevelopmentHistory.ts index ca169532..04cdf7b9 100644 --- a/src/entities/ProfileDevelopmentHistory.ts +++ b/src/entities/ProfileDevelopmentHistory.ts @@ -144,6 +144,13 @@ export class ProfileDevelopmentHistory extends EntityBase { }) profileDevelopmentId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูลการพัฒนารายบุคคล (Individual Development Plan)", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileDevelopment, (profileDevelopment) => profileDevelopment.profileDevelopmentHistories, diff --git a/src/entities/ProfileTraining.ts b/src/entities/ProfileTraining.ts index 2afaa17a..e887ce39 100644 --- a/src/entities/ProfileTraining.ts +++ b/src/entities/ProfileTraining.ts @@ -122,6 +122,13 @@ export class ProfileTraining extends EntityBase { }) developmentId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูลการฝึกอบรม/ดูงาน", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileTrainingHistory, (profileTrainingHistory) => profileTrainingHistory.histories, diff --git a/src/entities/ProfileTrainingHistory.ts b/src/entities/ProfileTrainingHistory.ts index 4ce0eada..f5dce05d 100644 --- a/src/entities/ProfileTrainingHistory.ts +++ b/src/entities/ProfileTrainingHistory.ts @@ -98,6 +98,13 @@ export class ProfileTrainingHistory extends EntityBase { }) isDate: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูลการฝึกอบรม/ดูงาน", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => ProfileTraining, (profileTraining) => profileTraining.profileTrainingHistories) @JoinColumn({ name: "profileTrainingId" }) histories: ProfileTraining; diff --git a/src/migration/1770110880489-update_tables_tranning_development_actPosition_add_column_isDeleted.ts b/src/migration/1770110880489-update_tables_tranning_development_actPosition_add_column_isDeleted.ts new file mode 100644 index 00000000..ed1d3f70 --- /dev/null +++ b/src/migration/1770110880489-update_tables_tranning_development_actPosition_add_column_isDeleted.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateTablesTranningDevelopmentActPositionAddColumnIsDeleted1770110880489 implements MigrationInterface { + name = 'UpdateTablesTranningDevelopmentActPositionAddColumnIsDeleted1770110880489' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileTraining\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูลการฝึกอบรม/ดูงาน' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileDevelopment\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูลการพัฒนารายบุคคล (Individual Development Plan)' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileActposition\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูลรักษาการในตำแหน่ง' DEFAULT 0`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileActposition\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileDevelopment\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileTraining\` DROP COLUMN \`isDeleted\``); + } + +} diff --git a/src/migration/1770112472041-update_tables_history_tranning_development_actPosition_add_column_isDeleted.ts b/src/migration/1770112472041-update_tables_history_tranning_development_actPosition_add_column_isDeleted.ts new file mode 100644 index 00000000..6d33f623 --- /dev/null +++ b/src/migration/1770112472041-update_tables_history_tranning_development_actPosition_add_column_isDeleted.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateTablesHistoryTranningDevelopmentActPositionAddColumnIsDeleted1770112472041 implements MigrationInterface { + name = 'UpdateTablesHistoryTranningDevelopmentActPositionAddColumnIsDeleted1770112472041' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileTrainingHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูลการฝึกอบรม/ดูงาน' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileDevelopmentHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูลการพัฒนารายบุคคล (Individual Development Plan)' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileActpositionHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูลรักษาการในตำแหน่ง' DEFAULT 0`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileActpositionHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileDevelopmentHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileTrainingHistory\` DROP COLUMN \`isDeleted\``); + } + +} From 0b09a99e42a36d5538ae1954a664ed42d442441f Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Tue, 3 Feb 2026 18:03:19 +0700 Subject: [PATCH 034/178] add: sync-users-to-keycloak --- scripts/sync-users-to-keycloak.ts | 327 ++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 scripts/sync-users-to-keycloak.ts diff --git a/scripts/sync-users-to-keycloak.ts b/scripts/sync-users-to-keycloak.ts new file mode 100644 index 00000000..93b4660f --- /dev/null +++ b/scripts/sync-users-to-keycloak.ts @@ -0,0 +1,327 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { Profile } from "../src/entities/Profile"; +import { RoleKeycloak } from "../src/entities/RoleKeycloak"; +import * as keycloak from "../src/keycloak/index"; + +// Default role for users without roles +const DEFAULT_ROLE = "USER"; + +interface SyncOptions { + dryRun: boolean; + delay: number; +} + +interface SyncResult { + total: number; + deleted: number; + created: number; + failed: number; + skipped: number; + errors: Array<{ + profileId: string; + citizenId: string; + error: string; + }>; +} + +/** + * Delete all Keycloak users (except super_admin) + */ +async function deleteAllKeycloakUsers(dryRun: boolean): Promise { + const users = await keycloak.getUserList("0", "-1"); + let count = 0; + let skipped = 0; + + if (!users || typeof users === "boolean") { + return 0; + } + + for (const user of users) { + // Skip super_admin user + if (user.username === "super_admin") { + console.log(`Skipped super_admin user (protected)`); + skipped++; + continue; + } + + if (!dryRun) { + await keycloak.deleteUser(user.id); + } + count++; + console.log(`[${count}/${users.length}] Deleted user: ${user.username}`); + } + + console.log(`Skipped ${skipped} protected user(s)`); + return count; +} + +/** + * Sync profiles to Keycloak + */ +async function syncProfiles(options: SyncOptions): Promise { + const result: SyncResult = { + total: 0, + deleted: 0, + created: 0, + failed: 0, + skipped: 0, + errors: [], + }; + + // Fetch all Keycloak roles first + const keycloakRoles = await keycloak.getRoles(); + if (!keycloakRoles || typeof keycloakRoles === "boolean") { + throw new Error("Failed to get Keycloak roles"); + } + const roleMap = new Map(keycloakRoles.map((r) => [r.name, r.id])); + + // Log all available Keycloak roles for debugging + console.log("Available Keycloak roles:", Array.from(roleMap.keys()).sort()); + console.log(""); + + // Get repositories + const profileRepo = AppDataSource.getRepository(Profile); + const roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); + + // Query all active profiles (not just those with keycloak set) + const profiles = await profileRepo + .createQueryBuilder("profile") + .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloak") + .andWhere("profile.isActive = :isActive", { isActive: true }) + .getMany(); + + result.total = profiles.length; + + for (const profile of profiles) { + const index = result.created + result.failed + result.skipped + 1; + + try { + // Validate required fields + if (!profile.citizenId) { + console.log(`[${index}/${result.total}] Skipped: Missing citizenId`); + result.skipped++; + continue; + } + + // Check if user already exists + const existingUser = await keycloak.getUserByUsername(profile.citizenId); + if (existingUser && existingUser.length > 0) { + const existingUserId = existingUser[0].id; + console.log( + `[${index}/${result.total}] User ${profile.citizenId} already exists in Keycloak (ID: ${existingUserId})`, + ); + + // Update profile.keycloak with existing Keycloak ID + if (!options.dryRun) { + await profileRepo.update(profile.id, { keycloak: existingUserId }); + console.log(` -> Updated profile.keycloak: ${existingUserId}`); + } + + result.skipped++; + continue; + } + + // Handle roles: assign USER role if no roles exist + let rolesToAssign = profile.roleKeycloaks || []; + let needsDefaultRole = false; + + if (rolesToAssign.length === 0) { + needsDefaultRole = true; + console.log( + `[${index}/${result.total}] No roles found for ${profile.citizenId}, will assign ${DEFAULT_ROLE}`, + ); + + // Check if USER role exists in Keycloak + if (!roleMap.has(DEFAULT_ROLE)) { + console.log( + `[${index}/${result.total}] ERROR: ${DEFAULT_ROLE} role not found in Keycloak`, + ); + result.failed++; + result.errors.push({ + profileId: profile.id, + citizenId: profile.citizenId, + error: `${DEFAULT_ROLE} role not found in Keycloak`, + }); + continue; + } + + // In live mode, create role in database + if (!options.dryRun) { + // Check if USER role record exists in database + const userRoleRecord = await roleKeycloakRepo.findOne({ + where: { name: DEFAULT_ROLE }, + }); + + // Assign role to profile in database + if (userRoleRecord) { + profile.roleKeycloaks = [userRoleRecord]; + await profileRepo.save(profile); + rolesToAssign = [userRoleRecord]; + } + } + } + + if (!options.dryRun) { + // Create user in Keycloak + const userId = await keycloak.createUser(profile.citizenId, "P@ssw0rd", { + firstName: profile.firstName || "", + lastName: profile.lastName || "", + // email: profile.email || undefined, + enabled: true, + }); + + if (typeof userId === "string") { + // Update profile.keycloak with new ID + await profileRepo.update(profile.id, { keycloak: userId }); + + // Assign roles to user in Keycloak + // Track which roles exist in Keycloak and which don't + const validRoles: { id: string; name: string }[] = []; + const missingRoles: string[] = []; + + for (const rk of rolesToAssign) { + const roleId = roleMap.get(rk.name); + if (roleId) { + validRoles.push({ id: roleId, name: rk.name }); + } else { + missingRoles.push(rk.name); + } + } + + // Warn about missing roles + if (missingRoles.length > 0) { + console.log(` [WARNING] Roles not found in Keycloak: ${missingRoles.join(", ")}`); + } + + if (validRoles.length > 0) { + const addRolesResult = await keycloak.addUserRoles(userId, validRoles); + if (addRolesResult === false) { + console.log(` [WARNING] Failed to assign roles to user ${profile.citizenId}`); + } + const roleNames = validRoles.map((r) => r.name).join(", "); + console.log( + `[${index}/${result.total}] Created: ${profile.citizenId} -> ${userId} [Roles: ${roleNames}]`, + ); + } else { + console.log( + `[${index}/${result.total}] Created: ${profile.citizenId} -> ${userId} [No roles assigned]`, + ); + } + + result.created++; + } else { + console.log(`[${index}/${result.total}] Failed: ${profile.citizenId}`, userId); + result.failed++; + result.errors.push({ + profileId: profile.id, + citizenId: profile.citizenId, + error: JSON.stringify(userId), + }); + } + } else { + // Dry-run mode - check which roles are valid + const validRoles: string[] = []; + const missingRoles: string[] = []; + + for (const rk of rolesToAssign) { + if (roleMap.has(rk.name)) { + validRoles.push(rk.name); + } else { + missingRoles.push(rk.name); + } + } + + if (needsDefaultRole && roleMap.has(DEFAULT_ROLE)) { + validRoles.push(DEFAULT_ROLE); + } else if (needsDefaultRole) { + missingRoles.push(DEFAULT_ROLE); + } + + const roleNames = validRoles.length > 0 ? validRoles.join(", ") : "None"; + console.log( + `[DRY-RUN ${index}/${result.total}] Would create: ${profile.citizenId} [Roles: ${roleNames}]`, + ); + + if (missingRoles.length > 0) { + console.log(` [WARNING] Roles not found in Keycloak: ${missingRoles.join(", ")}`); + } + + result.created++; + } + + // Delay to avoid rate limiting + if (options.delay > 0) { + await new Promise((resolve) => setTimeout(resolve, options.delay)); + } + } catch (error) { + console.log(`[${index}/${result.total}] Error: ${profile.citizenId}`, error); + result.failed++; + result.errors.push({ + profileId: profile.id, + citizenId: profile.citizenId, + error: String(error), + }); + } + } + + return result; +} + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + + console.log("=".repeat(60)); + console.log("Keycloak User Sync Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + console.log(""); + + // Initialize database + await AppDataSource.initialize(); + console.log("Database connected"); + + // Validate Keycloak connection + await keycloak.getToken(); + console.log("Keycloak connected"); + console.log(""); + + // Step 1: Delete existing users + console.log("Step 1: Deleting existing Keycloak users..."); + const deletedCount = await deleteAllKeycloakUsers(dryRun); + console.log(`Deleted ${deletedCount} users\n`); + + // Step 2: Sync profiles + console.log("Step 2: Creating users from profiles..."); + const result = await syncProfiles({ dryRun, delay: 100 }); + console.log(""); + + // Summary + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total profiles: ${result.total}`); + console.log(` Deleted users: ${deletedCount}`); + console.log(` Created users: ${result.created}`); + console.log(` Failed: ${result.failed}`); + console.log(` Skipped: ${result.skipped}`); + console.log("=".repeat(60)); + + if (result.errors.length > 0) { + console.log("\nErrors:"); + result.errors.forEach((e) => { + console.log(` ${e.citizenId}: ${e.error}`); + }); + } + + await AppDataSource.destroy(); +} + +main().catch(console.error); + +// add this line to package.json scripts section: +// "sync-keycloak": "ts-node scripts/sync-users-to-keycloak-null-only.ts", +// "sync-keycloak:dry": "ts-node scripts/sync-users-to-keycloak-null-only.ts --dry-run" From 22639e72c606051811732b71b7e686753150ec1c Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Wed, 4 Feb 2026 10:43:11 +0700 Subject: [PATCH 035/178] #migrate : add email phone To table issues --- src/entities/Issues.ts | 10 ++++++++ ...1770173342631-AddEmailPhoneToIssuesOnly.ts | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/migration/1770173342631-AddEmailPhoneToIssuesOnly.ts diff --git a/src/entities/Issues.ts b/src/entities/Issues.ts index 8bf3143e..aff597e1 100644 --- a/src/entities/Issues.ts +++ b/src/entities/Issues.ts @@ -30,6 +30,12 @@ export class Issues extends EntityBase { @Column({ type: "text", nullable: true, comment: "หมายเหตุ" }) remark: string | null; + @Column({ type: "varchar", nullable: true, length: 255, comment: "อีเมลผู้รายงาน" }) + email: string | null; + + @Column({ type: "varchar", nullable: true, length: 20, comment: "เบอร์โทรผู้รายงาน" }) + phone: string | null; + @Column({ type: "enum", enum: ["NEW", "IN_PROGRESS", "RESOLVED", "CLOSED"], @@ -76,6 +82,8 @@ export interface IssueResponse { lastUpdatedAt: Date; createdFullName: string; lastUpdateFullName: string; + email: string | null; + phone: string | null; } export interface CreateIssueRequest { @@ -85,6 +93,8 @@ export interface CreateIssueRequest { status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED"; menu?: string; org?: string; + email?: string; + phone?: string; } export interface UpdateIssueRequest { diff --git a/src/migration/1770173342631-AddEmailPhoneToIssuesOnly.ts b/src/migration/1770173342631-AddEmailPhoneToIssuesOnly.ts new file mode 100644 index 00000000..e379262d --- /dev/null +++ b/src/migration/1770173342631-AddEmailPhoneToIssuesOnly.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddEmailPhoneToIssues1738627400000 implements MigrationInterface { + name = 'AddEmailPhoneToIssues1738627400000' + + public async up(queryRunner: QueryRunner): Promise { + + await queryRunner.query(` + ALTER TABLE \`issues\` + ADD \`email\` varchar(255) NULL COMMENT 'อีเมลผู้รายงาน' + `); + + await queryRunner.query(` + ALTER TABLE \`issues\` + ADD \`phone\` varchar(20) NULL COMMENT 'เบอร์โทรผู้รายงาน' + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`issues\` DROP COLUMN \`phone\``); + await queryRunner.query(`ALTER TABLE \`issues\` DROP COLUMN \`email\``); + } +} \ No newline at end of file From e76e3619811ef18df6779f4f12b4ea1391ba229e Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 4 Feb 2026 16:18:45 +0700 Subject: [PATCH 036/178] init claude overview project --- CLAUDE.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1db8de32 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,137 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is **bma-ehr-organization** - an HRMS (Human Resource Management System) API backend for a Thai healthcare organization. It manages personnel data, organizational structure, positions, and employee profiles for an Electronic Health Record (EHR) system. + +- **Type**: RESTful API backend with WebSocket support +- **Language**: TypeScript/Node.js +- **Database**: MySQL with TypeORM +- **API Framework**: TSOA (TypeScript OpenAPI) with auto-generated routes and Swagger docs + +## Common Commands + +### Development +```bash +npm run dev # Start development server with hot-reload (nodemon) +npm run build # Build for production (runs tsoa spec-and-routes && tsc) +npm start # Run production build (node ./dist/app.js) +npm run format # Format code with Prettier +npm run check # Type check without emitting (tsc --noEmit) +``` + +### Database Migrations + +**CRITICAL**: After generating any migration, you MUST run the cleanup script to remove FK/idx lines: + +```bash +# Generate new migration (include descriptive name) +npm run migration:generate src/migration/update_table_0811202s + +# CLEANUP: Remove FK_/idx_ lines from generated migration +node scripts/clean-migration-fk-idx.js + +# Run migrations +npm run migration:run +``` + +The cleanup script replaces lines containing `FK_` or `idx_` with `// removed FK_/idx_ auto-cleanup`. This is required because TypeORM generates foreign key and index constraints that must be manually removed. + +### Local GitHub Actions Testing +```bash +# Test release workflow locally using act +act workflow_dispatch -W .github/workflows/release.yaml \ + --input IMAGE_VER=latest \ + -s DOCKER_USER=admin \ + -s DOCKER_PASS=FPTadmin2357 \ + -s SSH_PASSWORD=FPTadmin2357 +``` + +## Architecture + +### Application Entry Point +`src/app.ts` - Initializes the application: +1. Database connection (TypeORM MySQL) +2. In-memory caches (LogMemoryStore, OrgStructureCache with 30min TTL) +3. WebSocket server for real-time updates +4. Express middleware and TSOA routes +5. Cronjobs (scheduled tasks) +6. RabbitMQ message queue consumer + +### Controllers (`src/controllers/`) +Handle HTTP requests. Main controllers include: +- `OrganizationController` - Organizational structure management +- `CommandController` - Workflow and command processing +- `ProfileSalaryController` - Salary and tenure management +- Controllers for positions, employees, auth, etc. + +Cronjob logic is embedded in controllers (not separate services). + +### Services (`src/services/`) +Business logic and external integrations: +- `OrganizationService.ts` - Core organizational logic +- `rabbitmq.ts` - RabbitMQ consumer for async processing +- `webSocket.ts` - Real-time updates to clients + +### Entities (`src/entities/`) +TypeORM database models for MySQL. Key entities: +- Organization hierarchy (OrgRoot, OrgChild1-4) +- Position management (Position, PosType, PosLevel, PosExecutive) +- Employee profiles and related data +- Command/Workflow entities + +### Middlewares (`src/middlewares/`) +- `auth.ts` - Keycloak Bearer token authentication +- `authWebService.ts` - API key authentication (`X-API-Key` header) +- `role.ts` - Role-based authorization +- `logs.ts` - Request logging +- `error.ts` - Global error handling +- `user.ts` - User context extraction + +### TSOA Configuration +`src/routes.ts` is auto-generated by TSOA from controller decorators. Regenerated by `npm run build`. + +- Swagger docs available at `/api-docs` +- Dual authentication: Keycloak bearer tokens and API keys +- API tags organized by domain (Organization, Position, Employee, etc.) + +## Scheduled Cronjobs + +All times in Bangkok timezone (UTC+7): + +| Schedule | Task | Controller | +|----------|------|------------| +| `0 0 3 * * *` | Daily revision processing | `OrganizationController.cronjobRevision()` | +| `0 0 2 * * *` | Daily command processing | `CommandController.cronjobCommand()` | +| `0 0 1 10 *` | Monthly retirement status update (10th of month) | `CommandController.cronjobUpdateRetirementStatus()` | +| `0 0 0 * * *` | Daily tenure updates | `ProfileSalaryController` (multiple tenure methods) | + +## External Dependencies + +- **Keycloak** - Authentication and authorization +- **RabbitMQ** - Message queue for async operations +- **WebSocket** (Socket.IO) - Real-time updates +- **Elasticsearch** - Logging +- **Redis** - Caching layer +- **TypeORM** - Database ORM + +## Code Style + +- **Prettier**: 2 spaces, 100 char width, trailing commas +- **TypeScript**: Strict mode enabled +- **Comments**: Mixed Thai and English +- **Date handling**: Custom `DateSerializer` for local timezone serialization + +## Important Notes + +1. **Migration cleanup is mandatory** - TypeORM generates FK and index constraints that break the migration system. Always run `node scripts/clean-migration-fk-idx.js` after `migration:generate`. + +2. **Organization caching** - `OrgStructureCache` provides in-memory caching of org structure (30 min TTL) for performance. + +3. **Graceful shutdown** - Application handles SIGTERM/SIGINT to close database connections, caches, and HTTP server properly. + +4. **Dual auth system** - Most endpoints use Keycloak bearer tokens, but some web service endpoints use `X-API-Key` header authentication. + +5. **Thai localization** - The system is primarily for Thai users; documentation and some content is in Thai, but code is in English. From 55085ab8d8bfe918058bc87a9a3407e9836498c5 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 4 Feb 2026 16:32:25 +0700 Subject: [PATCH 037/178] =?UTF-8?q?=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=87=20?= =?UTF-8?q?isDeleted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 1 + src/controllers/PosMasterActController.ts | 2 +- src/controllers/ProfileActpositionEmployeeController.ts | 4 ++-- src/controllers/ProfileActpositionEmployeeTempController.ts | 4 ++-- src/controllers/ProfileController.ts | 2 +- src/controllers/ProfileDevelopmentEmployeeController.ts | 4 ++-- src/controllers/ProfileDevelopmentEmployeeTempController.ts | 4 ++-- src/controllers/ProfileEmployeeController.ts | 2 +- src/controllers/ProfileTrainingEmployeeController.ts | 4 ++-- src/controllers/ProfileTrainingEmployeeTempController.ts | 4 ++-- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 45bfa186..0c5a8653 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -7502,6 +7502,7 @@ export class CommandController extends Controller { where: { profileId: item.posMasterChild.current_holderId, status: true, + isDeleted: false }, }); diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 061d014e..7c63ede5 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -599,7 +599,7 @@ export class PosMasterActController extends Controller { "lastUpdatedAt", "dateEnd", ], - where: { profileId, status: true }, + where: { profileId, status: true, isDeleted: false }, }); if (existingActivePositions.length > 0) { diff --git a/src/controllers/ProfileActpositionEmployeeController.ts b/src/controllers/ProfileActpositionEmployeeController.ts index 2ada7dd3..d6afcebe 100644 --- a/src/controllers/ProfileActpositionEmployeeController.ts +++ b/src/controllers/ProfileActpositionEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileActpositionEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileActpositionId = await this.profileActpositionRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileActpositionId) { @@ -58,7 +58,7 @@ export class ProfileActpositionEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const getProfileActpositionId = await this.profileActpositionRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileActpositionId) { diff --git a/src/controllers/ProfileActpositionEmployeeTempController.ts b/src/controllers/ProfileActpositionEmployeeTempController.ts index 630cb785..1cf92baa 100644 --- a/src/controllers/ProfileActpositionEmployeeTempController.ts +++ b/src/controllers/ProfileActpositionEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileActpositionEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileActpositionId = await this.profileActpositionRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileActpositionId) { @@ -57,7 +57,7 @@ export class ProfileActpositionEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const getProfileActpositionId = await this.profileActpositionRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileActpositionId) { diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 4fe44943..41fe95e7 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1505,7 +1505,7 @@ export class ProfileController extends Controller { const actposition_raw = await this.profileActpositionRepo.find({ select: ["dateStart", "dateEnd", "position"], - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const assistance_raw = await this.profileAssistanceRepository.find({ diff --git a/src/controllers/ProfileDevelopmentEmployeeController.ts b/src/controllers/ProfileDevelopmentEmployeeController.ts index 76260a1b..2f5a7715 100644 --- a/src/controllers/ProfileDevelopmentEmployeeController.ts +++ b/src/controllers/ProfileDevelopmentEmployeeController.ts @@ -43,7 +43,7 @@ export class ProfileDevelopmentEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.developmentRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -65,7 +65,7 @@ export class ProfileDevelopmentEmployeeController extends Controller { let query = await AppDataSource.getRepository(ProfileDevelopment) .createQueryBuilder("profileDevelopment") - .where({ profileEmployeeId: profileId }) + .where({ profileEmployeeId: profileId, isDeleted: false }) .andWhere( new Brackets((qb) => { qb.where( diff --git a/src/controllers/ProfileDevelopmentEmployeeTempController.ts b/src/controllers/ProfileDevelopmentEmployeeTempController.ts index 84cfa074..9e8c2d26 100644 --- a/src/controllers/ProfileDevelopmentEmployeeTempController.ts +++ b/src/controllers/ProfileDevelopmentEmployeeTempController.ts @@ -41,7 +41,7 @@ export class ProfileDevelopmentEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.developmentRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -52,7 +52,7 @@ export class ProfileDevelopmentEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const lists = await this.developmentRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 008f0c67..e87adcac 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1501,7 +1501,7 @@ export class ProfileEmployeeController extends Controller { const actposition_raw = await this.profileActpositionRepo.find({ select: ["dateStart", "dateEnd", "position"], - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const assistance_raw = await this.profileAssistanceRepository.find({ diff --git a/src/controllers/ProfileTrainingEmployeeController.ts b/src/controllers/ProfileTrainingEmployeeController.ts index 60f1c2ac..d9f824a8 100644 --- a/src/controllers/ProfileTrainingEmployeeController.ts +++ b/src/controllers/ProfileTrainingEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileTrainingEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.trainingRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -52,7 +52,7 @@ export class ProfileTrainingEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const record = await this.trainingRepo.find({ - where: { profileEmployeeId }, + where: { profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileTrainingEmployeeTempController.ts b/src/controllers/ProfileTrainingEmployeeTempController.ts index 64e6fdcd..46e1686e 100644 --- a/src/controllers/ProfileTrainingEmployeeTempController.ts +++ b/src/controllers/ProfileTrainingEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileTrainingEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.trainingRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -51,7 +51,7 @@ export class ProfileTrainingEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const record = await this.trainingRepo.find({ - where: { profileEmployeeId }, + where: { profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); From a487b73c3bcfbf7402adc9933fa6f9bf514929b8 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 4 Feb 2026 16:52:39 +0700 Subject: [PATCH 038/178] setup auth middleware and sync code --- src/controllers/KeycloakSyncController.ts | 198 +++++++++++ src/keycloak/index.ts | 64 ++++ src/middlewares/auth.ts | 9 + src/middlewares/user.ts | 8 + src/services/KeycloakAttributeService.ts | 405 ++++++++++++++++++++++ 5 files changed, 684 insertions(+) create mode 100644 src/controllers/KeycloakSyncController.ts create mode 100644 src/services/KeycloakAttributeService.ts diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts new file mode 100644 index 00000000..2a55983c --- /dev/null +++ b/src/controllers/KeycloakSyncController.ts @@ -0,0 +1,198 @@ +import { Controller, Post, Get, Route, Security, Tags, Path, Request, Response, Query } from "tsoa"; +import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; +import { AppDataSource } from "../database/data-source"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; + +@Route("api/v1/org/keycloak-sync") +@Tags("Keycloak Sync") +@Security("bearerAuth") +@Response( + HttpStatus.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถดำเนินการได้ กรุณาลองใหม่ในภายหลัง", +) +export class KeycloakSyncController extends Controller { + private keycloakAttributeService = new KeycloakAttributeService(); + private profileRepo = AppDataSource.getRepository(Profile); + private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); + + /** + * Sync attributes for the current logged-in user + * + * @summary Sync profileId and rootDnaId to Keycloak for current user + */ + @Post("sync-me") + async syncCurrentUser(@Request() request: RequestWithUser) { + const keycloakUserId = request.user.sub; + + if (!keycloakUserId) { + throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); + } + + // Get attributes from database before sync + const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); + + const success = await this.keycloakAttributeService.syncUserAttributes(keycloakUserId); + + if (!success) { + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ กรุณาติดต่อผู้ดูแลระบบ", + ); + } + + // Verify sync by fetching attributes from Keycloak after update + const kcAttrsAfter = + await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); + + return new HttpSuccess({ + message: "Sync ข้อมูลสำเร็จ", + syncedToKeycloak: !!kcAttrsAfter?.profileId, + databaseAttributes: dbAttrs, + keycloakAttributesAfter: kcAttrsAfter, + }); + } + + /** + * Get current attributes of the logged-in user + * + * @summary Get current profileId and rootDnaId from Keycloak + */ + @Get("my-attributes") + async getMyAttributes(@Request() request: RequestWithUser) { + const keycloakUserId = request.user.sub; + + if (!keycloakUserId) { + throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); + } + + const keycloakAttributes = + await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); + const dbAttributes = + await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); + + return new HttpSuccess({ + keycloakAttributes, + databaseAttributes: dbAttributes, + }); + } + + /** + * Sync attributes for a specific profile (Admin only) + * + * @summary Sync profileId and rootDnaId to Keycloak by profile ID (ADMIN) + * + * @param {string} profileId Profile ID + * @param {string} profileType Profile type (PROFILE or PROFILE_EMPLOYEE) + */ + @Post("sync-profile/:profileId") + async syncByProfileId( + @Path() profileId: string, + @Query() profileType: "PROFILE" | "PROFILE_EMPLOYEE" = "PROFILE", + ) { + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + const success = await this.keycloakAttributeService.syncOnOrganizationChange( + profileId, + profileType, + ); + + if (!success) { + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ หรือไม่พบข้อมูล profile", + ); + } + + return new HttpSuccess({ message: "Sync ข้อมูลสำเร็จ" }); + } + + /** + * Batch sync all users (Admin only) + * + * @summary Batch sync all users to Keycloak (ADMIN) + * + * @param {number} limit Maximum number of users to sync + */ + @Post("sync-all") + async syncAll(@Query() limit: number = 100) { + if (limit > 500) { + throw new HttpError(HttpStatus.BAD_REQUEST, "limit ต้องไม่เกิน 500"); + } + + const result = await this.keycloakAttributeService.batchSyncUsers(limit); + + return new HttpSuccess({ + message: "Batch sync เสร็จสิ้น", + total: result.total, + success: result.success, + failed: result.failed, + details: result.details, + }); + } + + /** + * ตรวจสอบสถานะ Keycloak Mapper + * + * @summary ตรวจสอบว่า profileId และ rootDnaId ออกมาใน token หรือไม่ + */ + @Get("check-mapper") + async checkMapperStatus(@Request() request: RequestWithUser) { + const keycloakUserId = request.user.sub; + + if (!keycloakUserId) { + throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); + } + + // 1. ตรวจสอบ attributes ใน Keycloak + const kcAttrs = + await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); + + // 2. ตรวจสอบ attributes ใน Database + const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); + + // 3. ตรวจสอบ token payload ปัจจุบัน + const tokenPayload = request.user; + + return new HttpSuccess({ + keycloakAttributes: kcAttrs, + databaseAttributes: dbAttrs, + tokenHasProfileId: !!tokenPayload.profileId, + tokenHasOrgRootDnaId: !!tokenPayload.orgRootDnaId, + tokenScopes: tokenPayload.scope?.split(" ") || [], + diagnosis: { + kcHasProfileId: !!kcAttrs?.profileId, + kcHasOrgRootDnaId: !!kcAttrs?.orgRootDnaId, + kcHasOrgChild1DnaId: !!kcAttrs?.orgChild1DnaId, + kcHasOrgChild2DnaId: !!kcAttrs?.orgChild2DnaId, + kcHasOrgChild3DnaId: !!kcAttrs?.orgChild3DnaId, + kcHasOrgChild4DnaId: !!kcAttrs?.orgChild4DnaId, + kcHasEmpType: !!kcAttrs?.empType, + dbHasProfileId: !!dbAttrs?.profileId, + dbHasOrgRootDnaId: !!dbAttrs?.orgRootDnaId, + dbHasOrgChild1DnaId: !!dbAttrs?.orgChild1DnaId, + dbHasOrgChild2DnaId: !!dbAttrs?.orgChild2DnaId, + dbHasOrgChild3DnaId: !!dbAttrs?.orgChild3DnaId, + dbHasOrgChild4DnaId: !!dbAttrs?.orgChild4DnaId, + dbHasEmpType: !!dbAttrs?.empType, + issue: + !tokenPayload.profileId && kcAttrs?.profileId + ? "Attribute มีใน Keycloak แต่ไม่ออกมาใน token - แก้ไข Mapper หรือ Client Scope" + : !kcAttrs?.profileId && dbAttrs?.profileId + ? "Attribute มีใน Database แต่ไม่มีใน Keycloak - ต้อง sync ซ้ำ" + : !dbAttrs?.profileId + ? "ไม่พบ profile ใน database - ตรวจสอบ keycloak field" + : "ทุกอย่างปกติ", + }, + }); + } +} diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index a81e2af9..d977c42d 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -772,6 +772,70 @@ export async function changeUserPassword(userId: string, newPassword: string) { } } +/** + * Update user attributes in Keycloak + * + * @param userId - Keycloak user ID + * @param attributes - Object containing attribute names and their values (as arrays) + * @returns true if success, false otherwise + */ +export async function updateUserAttributes( + userId: string, + attributes: Record, +): Promise { + try { + // Get existing user data to preserve other attributes + const existingUser = await getUser(userId); + + if (!existingUser) { + console.error(`User ${userId} not found in Keycloak`); + return false; + } + + // Merge existing attributes with new attributes + // Keycloak requires id to be present in the payload + const updatedAttributes = { + id: existingUser.id, + enabled: existingUser.enabled ?? true, + attributes: { + ...(existingUser.attributes || {}), + ...attributes, + }, + }; + + console.log(`[updateUserAttributes] Sending to Keycloak:`, JSON.stringify(updatedAttributes, null, 2)); + + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { + headers: { + authorization: `Bearer ${await getToken()}`, + "content-type": "application/json", + }, + method: "PUT", + body: JSON.stringify(updatedAttributes), + }).catch((e) => { + console.error(`[updateUserAttributes] Network error:`, e); + return null; + }); + + if (!res) { + console.error(`[updateUserAttributes] No response from Keycloak`); + return false; + } + + if (!res.ok) { + const errorText = await res.text(); + console.error(`[updateUserAttributes] Keycloak Error (${res.status}):`, errorText); + return false; + } + + console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`); + return true; + } catch (error) { + console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error); + return false; + } +} + // Function to reset password export async function resetPassword(username: string) { try { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index c27e6188..80fc9e92 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -75,6 +75,15 @@ export async function expressAuthentication( request.app.locals.logData.userName = payload.name; request.app.locals.logData.user = payload.preferred_username; + // เก็บค่า profileId และ orgRootDnaId จาก token (ใช้ค่าว่างถ้าไม่มี) + request.app.locals.logData.profileId = payload.profileId ?? ""; + request.app.locals.logData.orgRootDnaId = payload.orgRootDnaId ?? ""; + request.app.locals.logData.orgChild1DnaId = payload.orgChild1DnaId ?? ""; + request.app.locals.logData.orgChild2DnaId = payload.orgChild2DnaId ?? ""; + request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? ""; + request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? ""; + request.app.locals.logData.empType = payload.empType ?? ""; + return payload; } diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index e5c48d9a..43ac80a3 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -9,6 +9,14 @@ export type RequestWithUser = Request & { preferred_username: string; email: string; role: string[]; + profileId?: string; + orgRootDnaId?: string; + orgChild1DnaId?: string; + orgChild2DnaId?: string; + orgChild3DnaId?: string; + orgChild4DnaId?: string; + empType?: string; + scope?: string; }; }; diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts new file mode 100644 index 00000000..3ce04b1e --- /dev/null +++ b/src/services/KeycloakAttributeService.ts @@ -0,0 +1,405 @@ +import { AppDataSource } from "../database/data-source"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +// import { PosMaster } from "../entities/PosMaster"; +// import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +// import { OrgRoot } from "../entities/OrgRoot"; +import { getUser, updateUserAttributes } from "../keycloak"; +import { OrgRevision } from "../entities/OrgRevision"; + +export interface UserProfileAttributes { + profileId: string | null; + orgRootDnaId: string | null; + orgChild1DnaId: string | null; + orgChild2DnaId: string | null; + orgChild3DnaId: string | null; + orgChild4DnaId: string | null; + empType: string | null; +} + +/** + * Keycloak Attribute Service + * Service for syncing profileId and orgRootDnaId to Keycloak user attributes + */ +export class KeycloakAttributeService { + private profileRepo = AppDataSource.getRepository(Profile); + private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); + // private posMasterRepo = AppDataSource.getRepository(PosMaster); + // private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); + // private orgRootRepo = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); + + /** + * Get profile attributes (profileId and orgRootDnaId) from database + * Searches in Profile table first (ข้าราชการ), then ProfileEmployee (ลูกจ้าง) + * + * @param keycloakUserId - Keycloak user ID + * @returns UserProfileAttributes with profileId and orgRootDnaId + */ + async getUserProfileAttributes(keycloakUserId: string): Promise { + // First, try to find in Profile (ข้าราชการ) + const revisionCurrent = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true }, + }); + const revisionId = revisionCurrent ? revisionCurrent.id : null; + const profileResult = await this.profileRepo + .createQueryBuilder("p") + .leftJoinAndSelect("p.current_holders", "pm") + .leftJoinAndSelect("pm.orgRoot", "orgRoot") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("p.keycloak = :keycloakUserId", { keycloakUserId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) + .getOne(); + + if ( + profileResult && + profileResult.current_holders && + profileResult.current_holders.length > 0 + ) { + const currentPos = profileResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: "OFFICER", + }; + } + + // If not found in Profile, try ProfileEmployee (ลูกจ้าง) + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("epm.orgChild1", "orgChild1") + .leftJoinAndSelect("epm.orgChild2", "orgChild2") + .leftJoinAndSelect("epm.orgChild3", "orgChild3") + .leftJoinAndSelect("epm.orgChild4", "orgChild4") + .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + }; + } + + // Return null values if no profile found + return { + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + }; + } + + /** + * Get profile attributes by profile ID directly + * Used for syncing specific profiles + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง + * @returns UserProfileAttributes with profileId and orgRootDnaId + */ + async getAttributesByProfileId( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise { + const revisionCurrent = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true }, + }); + const revisionId = revisionCurrent ? revisionCurrent.id : null; + if (profileType === "PROFILE") { + const profileResult = await this.profileRepo + .createQueryBuilder("p") + .leftJoinAndSelect("p.current_holders", "pm") + .leftJoinAndSelect("pm.orgRoot", "orgRoot") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("p.id = :profileId", { profileId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) + .getOne(); + + if ( + profileResult && + profileResult.current_holders && + profileResult.current_holders.length > 0 + ) { + const currentPos = profileResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: "OFFICER", + }; + } + } else { + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("pe.id = :profileId", { profileId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + }; + } + } + + return { + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + }; + } + + /** + * Sync user attributes to Keycloak + * + * @param keycloakUserId - Keycloak user ID + * @returns true if sync successful, false otherwise + */ + async syncUserAttributes(keycloakUserId: string): Promise { + try { + const attributes = await this.getUserProfileAttributes(keycloakUserId); + + if (!attributes.profileId) { + console.log(`No profile found for Keycloak user ${keycloakUserId}`); + return false; + } + + // Prepare attributes for Keycloak (must be arrays) + const keycloakAttributes: Record = { + profileId: [attributes.profileId], + orgRootDnaId: [attributes.orgRootDnaId || ""], + orgChild1DnaId: [attributes.orgChild1DnaId || ""], + orgChild2DnaId: [attributes.orgChild2DnaId || ""], + orgChild3DnaId: [attributes.orgChild3DnaId || ""], + orgChild4DnaId: [attributes.orgChild4DnaId || ""], + empType: [attributes.empType || ""], + }; + + const success = await updateUserAttributes(keycloakUserId, keycloakAttributes); + + if (success) { + console.log(`Synced attributes for Keycloak user ${keycloakUserId}:`, attributes); + } + + return success; + } catch (error) { + console.error(`Error syncing attributes for Keycloak user ${keycloakUserId}:`, error); + return false; + } + } + + /** + * Sync attributes when organization changes + * This is called when a user moves to a different organization + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง + * @returns true if sync successful, false otherwise + */ + async syncOnOrganizationChange( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise { + try { + // Get the keycloak userId from the profile + let keycloakUserId: string | null = null; + + if (profileType === "PROFILE") { + const profile = await this.profileRepo.findOne({ where: { id: profileId } }); + keycloakUserId = profile?.keycloak || ""; + } else { + const profileEmployee = await this.profileEmployeeRepo.findOne({ + where: { id: profileId }, + }); + keycloakUserId = profileEmployee?.keycloak || ""; + } + + if (!keycloakUserId) { + console.log(`No Keycloak user ID found for profile ${profileId}`); + return false; + } + + return await this.syncUserAttributes(keycloakUserId); + } catch (error) { + console.error(`Error syncing organization change for profile ${profileId}:`, error); + return false; + } + } + + /** + * Batch sync multiple users + * Useful for initial sync or periodic updates + * + * @param limit - Maximum number of users to sync (default: 100) + * @returns Object with success count and details + */ + async batchSyncUsers( + limit: number = 100, + ): Promise<{ total: number; success: number; failed: number; details: any[] }> { + const result = { + total: 0, + success: 0, + failed: 0, + details: [] as any[], + }; + + try { + // Get profiles with keycloak IDs (ข้าราชการ) + const profiles = await this.profileRepo + .createQueryBuilder("p") + .where("p.keycloak IS NOT NULL") + .andWhere("p.keycloak != :empty", { empty: "" }) + .take(limit) + .getMany(); + + // Get profileEmployees with keycloak IDs (ลูกจ้าง) + const profileEmployees = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .where("pe.keycloak IS NOT NULL") + .andWhere("pe.keycloak != :empty", { empty: "" }) + .take(limit) + .getMany(); + + const allProfiles = [...profiles, ...profileEmployees]; + result.total = allProfiles.length; + + for (const profile of allProfiles) { + const keycloakUserId = profile.keycloak; + const profileType = profile instanceof Profile ? "PROFILE" : "PROFILE_EMPLOYEE"; + + try { + const success = await this.syncOnOrganizationChange(profile.id, profileType); + if (success) { + result.success++; + result.details.push({ + profileId: profile.id, + keycloakUserId, + status: "success", + }); + } else { + result.failed++; + result.details.push({ + profileId: profile.id, + keycloakUserId, + status: "failed", + error: "Sync returned false", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ + profileId: profile.id, + keycloakUserId, + status: "error", + error: error.message, + }); + } + } + } catch (error) { + console.error("Error in batch sync:", error); + } + + return result; + } + + /** + * Get current Keycloak attributes for a user + * + * @param keycloakUserId - Keycloak user ID + * @returns Current attributes from Keycloak + */ + async getCurrentKeycloakAttributes( + keycloakUserId: string, + ): Promise { + try { + const user = await getUser(keycloakUserId); + + if (!user || !user.attributes) { + return null; + } + + return { + profileId: user.attributes.profileId?.[0] || "", + orgRootDnaId: user.attributes.orgRootDnaId?.[0] || "", + orgChild1DnaId: user.attributes.orgChild1DnaId?.[0] || "", + orgChild2DnaId: user.attributes.orgChild2DnaId?.[0] || "", + orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "", + orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "", + empType: user.attributes.empType?.[0] || "", + }; + } catch (error) { + console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error); + return null; + } + } +} From dbc46e2fb93d14a429f6ed59b00faf9645e9668a Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 5 Feb 2026 10:14:49 +0700 Subject: [PATCH 039/178] =?UTF-8?q?=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=20return=20=E0=B8=AA=E0=B8=96=E0=B8=B2=E0=B8=99?= =?UTF-8?q?=E0=B8=B0=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=97=E0=B8=94=E0=B8=A5?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=E0=B8=87=E0=B8=B2=E0=B8=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/OrganizationDotnetController.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 1d4ef30a..0a2f40e0 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -1770,6 +1770,7 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, + isProbatin: profile.isProbation, posType: profile.posType?.posTypeName ?? null, posLevel: profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null @@ -1885,6 +1886,7 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, + isProbatin: profile.isProbation, posLevel: profile.posLevel?.posLevelName ?? null, posType: profile.posType?.posTypeName ?? null, @@ -2050,6 +2052,7 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, + isProbatin: profile.isProbation, posType: profile.posType?.posTypeName ?? null, posLevel: profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null @@ -2215,6 +2218,7 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, + isProbatin: profile.isProbation, posLevel: profile.posLevel?.posLevelName ?? null, posType: profile.posType?.posTypeName ?? null, From 39a07482cddb311b9081e706269e97611191b45a Mon Sep 17 00:00:00 2001 From: Adisak Date: Thu, 5 Feb 2026 11:23:38 +0700 Subject: [PATCH 040/178] add api find dna by keycloak --- src/controllers/OrganizationController.ts | 1603 +++++++++++---------- 1 file changed, 836 insertions(+), 767 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index ad1f71d5..f19be17b 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -280,6 +280,75 @@ export class OrganizationController extends Controller { return new HttpSuccess(requestBody); } + /** + * API สร้างแบบร่างโครงสร้าง + * + * @summary ORG_022 - สร้างโครงสร้างใหม่ #23 + * + */ + @Post("finddna-by-keycloak/{keycloakId}") + async FindDnaOrgByKeycloakId( + @Path() keycloakId: string + ) { + + let reply: any; + + const profileByKeycloak: any = await this.profileRepo.findOne({ + where: { keycloak: keycloakId }, + select: ["id", "keycloak"] + }) + + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); + + const posMaster = await AppDataSource.getRepository(PosMaster) + .createQueryBuilder("pos") + .leftJoin("pos.orgRoot", "orgRoot") + .leftJoin("pos.orgChild1", "orgChild1") + .leftJoin("pos.orgChild2", "orgChild2") + .leftJoin("pos.orgChild3", "orgChild3") + .leftJoin("pos.orgChild4", "orgChild4") + .where("pos.current_holderId = :holderId", { + holderId: profileByKeycloak.id, + }) + .andWhere("pos.orgRevisionId = :revId", { + revId: orgRevision?.id, + }) + .select([ + "pos.id", + "orgRoot.ancestorDNA as rootDnaId", + "orgChild1.ancestorDNA as child1DnaId", + "orgChild2.ancestorDNA as child2DnaId", + "orgChild3.ancestorDNA as child3DnaId", + "orgChild4.ancestorDNA as child4DnaId", + ]) + .getRawOne(); + + if (!posMaster) { + reply = { + rootDnaId: null, + child1DnaId: null, + child2DnaId: null, + child3DnaId: null, + child4DnaId: null, + }; + } else { + reply = { + rootDnaId: posMaster.rootDnaId, + child1DnaId: posMaster.child1DnaId, + child2DnaId: posMaster.child2DnaId, + child3DnaId: posMaster.child3DnaId, + child4DnaId: posMaster.child4DnaId, + }; + } + return new HttpSuccess(reply); + } + /** * API เช็คสถานะโครงสร้างว่าส่งคนไปออกคำสั่งหรือไม่ * @@ -381,157 +450,157 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - // .andWhere( - // _data.child1 != undefined && _data.child1 != null - // ? _data.child1[0] != null - // ? `orgChild1.id IN (:...node)` - // : `orgChild1.id is null` - // : "1=1", - // { - // node: _data.child1, - // }, - // ) - .select([ - "orgChild1.id", - "orgChild1.isOfficer", - "orgChild1.isInformation", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + // .andWhere( + // _data.child1 != undefined && _data.child1 != null + // ? _data.child1[0] != null + // ? `orgChild1.id IN (:...node)` + // : `orgChild1.id is null` + // : "1=1", + // { + // node: _data.child1, + // }, + // ) + .select([ + "orgChild1.id", + "orgChild1.isOfficer", + "orgChild1.isInformation", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - // .andWhere( - // _data.child2 != undefined && _data.child2 != null - // ? _data.child2[0] != null - // ? `orgChild2.id IN (:...node)` - // : `orgChild2.id is null` - // : "1=1", - // { - // node: _data.child2, - // }, - // ) - .select([ - "orgChild2.id", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + // .andWhere( + // _data.child2 != undefined && _data.child2 != null + // ? _data.child2[0] != null + // ? `orgChild2.id IN (:...node)` + // : `orgChild2.id is null` + // : "1=1", + // { + // node: _data.child2, + // }, + // ) + .select([ + "orgChild2.id", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - // .andWhere( - // _data.child3 != undefined && _data.child3 != null - // ? _data.child3[0] != null - // ? `orgChild3.id IN (:...node)` - // : `orgChild3.id is null` - // : "1=1", - // { - // node: _data.child3, - // }, - // ) - .select([ - "orgChild3.id", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + // .andWhere( + // _data.child3 != undefined && _data.child3 != null + // ? _data.child3[0] != null + // ? `orgChild3.id IN (:...node)` + // : `orgChild3.id is null` + // : "1=1", + // { + // node: _data.child3, + // }, + // ) + .select([ + "orgChild3.id", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - // .andWhere( - // _data.child4 != undefined && _data.child4 != null - // ? _data.child4[0] != null - // ? `orgChild4.id IN (:...node)` - // : `orgChild4.id is null` - // : "1=1", - // { - // node: _data.child4, - // }, - // ) - .select([ - "orgChild4.id", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + // .andWhere( + // _data.child4 != undefined && _data.child4 != null + // ? _data.child4[0] != null + // ? `orgChild4.id IN (:...node)` + // : `orgChild4.id is null` + // : "1=1", + // { + // node: _data.child4, + // }, + // ) + .select([ + "orgChild4.id", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; const formattedData = await Promise.all( @@ -1184,13 +1253,13 @@ export class OrganizationController extends Controller { where: orgRevision.orgRevisionIsCurrent && !orgRevision.orgRevisionIsDraft ? { - orgRevisionId: id, - current_holderId: profile.id, - } + orgRevisionId: id, + current_holderId: profile.id, + } : { - orgRevisionId: id, - next_holderId: profile.id, - }, + orgRevisionId: id, + next_holderId: profile.id, + }, }); if (!posMaster) return new HttpSuccess([]); @@ -1667,161 +1736,161 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 !== undefined && _data.child1 !== null - ? _data.child1[0] !== null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is null` - : "1=1", - { - node: _data.child1, - }, - ) - .select([ - "orgChild1.id", - "orgChild1.misId", - "orgChild1.isOfficer", - "orgChild1.isInformation", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 !== undefined && _data.child1 !== null + ? _data.child1[0] !== null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is null` + : "1=1", + { + node: _data.child1, + }, + ) + .select([ + "orgChild1.id", + "orgChild1.misId", + "orgChild1.isOfficer", + "orgChild1.isInformation", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 !== undefined && _data.child2 !== null - ? _data.child2[0] !== null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .select([ - "orgChild2.id", - "orgChild2.misId", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 !== undefined && _data.child2 !== null + ? _data.child2[0] !== null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .select([ + "orgChild2.id", + "orgChild2.misId", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 !== undefined && _data.child3 !== null - ? _data.child3[0] !== null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .select([ - "orgChild3.id", - "orgChild3.misId", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 !== undefined && _data.child3 !== null + ? _data.child3[0] !== null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .select([ + "orgChild3.id", + "orgChild3.misId", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 !== undefined && _data.child4 !== null - ? _data.child4[0] !== null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .select([ - "orgChild4.id", - "orgChild4.misId", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 !== undefined && _data.child4 !== null + ? _data.child4[0] !== null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .select([ + "orgChild4.id", + "orgChild4.misId", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; const formattedData = await Promise.all( @@ -3373,18 +3442,18 @@ export class OrganizationController extends Controller { const formattedData_ = rootId === "root" ? [ - { - personID: "", - name: "", - avatar: "", - positionName: "", - positionNum: "", - positionNumInt: null, - departmentName: data.orgRevisionName, - organizationId: data.id, - children: formattedData, - }, - ] + { + personID: "", + name: "", + avatar: "", + positionName: "", + positionNum: "", + positionNumInt: null, + departmentName: data.orgRevisionName, + organizationId: data.id, + children: formattedData, + }, + ] : formattedData; return new HttpSuccess(formattedData_); } @@ -3424,17 +3493,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3462,19 +3531,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3507,21 +3576,21 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3557,23 +3626,23 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3612,25 +3681,25 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: // await this.posMasterRepository.count({ // where: { @@ -3675,27 +3744,27 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: // await this.posMasterRepository.count({ // where: { @@ -3760,17 +3829,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -3798,19 +3867,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -3843,21 +3912,21 @@ export class OrganizationController extends Controller { totalPositionCurrentVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -3893,23 +3962,23 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -3948,25 +4017,25 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: // await this.posMasterRepository.count({ // where: { @@ -4024,17 +4093,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild1Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild1Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild1Id: data.id, @@ -4062,19 +4131,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild1Id: data.id, + orgChild2Id: orgChild2.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild1Id: data.id, + orgChild2Id: orgChild2.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild1Id: data.id, @@ -4107,21 +4176,21 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -4157,23 +4226,23 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -4221,17 +4290,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild2Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild2Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild2Id: data.id, @@ -4259,19 +4328,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild2Id: data.id, @@ -4304,21 +4373,21 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild2Id: data.id, @@ -4362,17 +4431,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild3Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild3Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild3Id: data.id, @@ -4400,19 +4469,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild3Id: data.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild3Id: data.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild3Id: data.id, @@ -4452,17 +4521,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild4Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild4Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild4Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild4Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild4Id: data.id, @@ -5414,88 +5483,88 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - // .andWhere( - // _data.child1 != undefined && _data.child1 != null - // ? _data.child1[0] != null - // ? `orgChild1.id IN (:...node)` - // : `orgChild1.id is null` - // : "1=1", - // { - // node: _data.child1, - // }, - // ) - .leftJoinAndSelect("orgChild1.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + // .andWhere( + // _data.child1 != undefined && _data.child1 != null + // ? _data.child1[0] != null + // ? `orgChild1.id IN (:...node)` + // : `orgChild1.id is null` + // : "1=1", + // { + // node: _data.child1, + // }, + // ) + .leftJoinAndSelect("orgChild1.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - // .andWhere( - // _data.child2 != undefined && _data.child2 != null - // ? _data.child2[0] != null - // ? `orgChild2.id IN (:...node)` - // : `orgChild2.id is null` - // : "1=1", - // { - // node: _data.child2, - // }, - // ) - .leftJoinAndSelect("orgChild2.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + // .andWhere( + // _data.child2 != undefined && _data.child2 != null + // ? _data.child2[0] != null + // ? `orgChild2.id IN (:...node)` + // : `orgChild2.id is null` + // : "1=1", + // { + // node: _data.child2, + // }, + // ) + .leftJoinAndSelect("orgChild2.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - // .andWhere( - // _data.child3 != undefined && _data.child3 != null - // ? _data.child3[0] != null - // ? `orgChild3.id IN (:...node)` - // : `orgChild3.id is null` - // : "1=1", - // { - // node: _data.child3, - // }, - // ) - .leftJoinAndSelect("orgChild3.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + // .andWhere( + // _data.child3 != undefined && _data.child3 != null + // ? _data.child3[0] != null + // ? `orgChild3.id IN (:...node)` + // : `orgChild3.id is null` + // : "1=1", + // { + // node: _data.child3, + // }, + // ) + .leftJoinAndSelect("orgChild3.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - // .andWhere( - // _data.child4 != undefined && _data.child4 != null - // ? _data.child4[0] != null - // ? `orgChild4.id IN (:...node)` - // : `orgChild4.id is null` - // : "1=1", - // { - // node: _data.child4, - // }, - // ) - .leftJoinAndSelect("orgChild4.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + // .andWhere( + // _data.child4 != undefined && _data.child4 != null + // ? _data.child4[0] != null + // ? `orgChild4.id IN (:...node)` + // : `orgChild4.id is null` + // : "1=1", + // { + // node: _data.child4, + // }, + // ) + .leftJoinAndSelect("orgChild4.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; // const formattedData = orgRootData.map((orgRoot) => { @@ -5945,159 +6014,159 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 != undefined && _data.child1 != null - ? _data.child1[0] != null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : "1=1", - { - node: _data.child1, - }, - ) - .select([ - "orgChild1.id", - "orgChild1.ancestorDNA", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 != undefined && _data.child1 != null + ? _data.child1[0] != null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : "1=1", + { + node: _data.child1, + }, + ) + .select([ + "orgChild1.id", + "orgChild1.ancestorDNA", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .select([ - "orgChild2.id", - "orgChild2.ancestorDNA", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 != undefined && _data.child2 != null + ? _data.child2[0] != null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .select([ + "orgChild2.id", + "orgChild2.ancestorDNA", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .select([ - "orgChild3.id", - "orgChild3.ancestorDNA", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 != undefined && _data.child3 != null + ? _data.child3[0] != null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .select([ + "orgChild3.id", + "orgChild3.ancestorDNA", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .select([ - "orgChild4.id", - "orgChild4.ancestorDNA", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 != undefined && _data.child4 != null + ? _data.child4[0] != null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .select([ + "orgChild4.id", + "orgChild4.ancestorDNA", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; // const formattedData = orgRootData.map((orgRoot) => { From 561dc7f66c17fc10c2f8ee5650a4c53af91338e1 Mon Sep 17 00:00:00 2001 From: Adisak Date: Thu, 5 Feb 2026 11:43:59 +0700 Subject: [PATCH 041/178] change method --- src/controllers/OrganizationController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index f19be17b..8ee5aa4f 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -286,7 +286,7 @@ export class OrganizationController extends Controller { * @summary ORG_022 - สร้างโครงสร้างใหม่ #23 * */ - @Post("finddna-by-keycloak/{keycloakId}") + @Get("finddna-by-keycloak/{keycloakId}") async FindDnaOrgByKeycloakId( @Path() keycloakId: string ) { From d0241016fb06ae5df9789a12ba74fee61dc53e37 Mon Sep 17 00:00:00 2001 From: Adisak Date: Thu, 5 Feb 2026 11:45:10 +0700 Subject: [PATCH 042/178] disciption --- src/controllers/OrganizationController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8ee5aa4f..8423bdd3 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -281,9 +281,9 @@ export class OrganizationController extends Controller { } /** - * API สร้างแบบร่างโครงสร้าง + * API หา ORG DNA ID * - * @summary ORG_022 - สร้างโครงสร้างใหม่ #23 + * @summary หา ORG DNA ID * */ @Get("finddna-by-keycloak/{keycloakId}") From 631d634074a42c5657b02007dd9c4d87b5186bc2 Mon Sep 17 00:00:00 2001 From: Adisak Date: Thu, 5 Feb 2026 13:37:28 +0700 Subject: [PATCH 043/178] #2239 --- src/controllers/OrganizationController.ts | 174 ++++++++++++++-------- 1 file changed, 115 insertions(+), 59 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8423bdd3..8799d8cd 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -5443,37 +5443,93 @@ export class OrganizationController extends Controller { */ @Get("act/{id}") async detailAct(@Path() id: string, @Request() request: RequestWithUser) { - // let _data = { - // root: null, - // child1: null, - // child2: null, - // child3: null, - // child4: null, - // }; - + let _data: any = { + root: null, + child1: null, + child2: null, + child3: null, + child4: null, + }; // if (!request.user.role.includes("SUPER_ADMIN")) { // _data = await new permission().PermissionOrgList(request, "SYS_ACTING"); // } - await new permission().PermissionOrgList(request, "SYS_ACTING"); + const _privilege = await new permission().PermissionOrgList(request, "SYS_ACTING"); const orgRevision = await this.orgRevisionRepository.findOne({ where: { id } }); if (!orgRevision) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล"); } + const attrOwnership = _privilege.root === null ? true : false; + + const profile = await this.profileRepo.findOne({ + where: { keycloak: request.user.sub }, + relations: ["permissionProfiles", "current_holders", "current_holders.posMasterAssigns"], + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งานในทะเบียนประวัติ"); + } + let profileAssign = profile.current_holders + ?.find((x) => x.orgRevisionId === id) + ?.posMasterAssigns.find((x) => x.assignId === "SYS_ORG"); + + if (orgRevision.orgRevisionIsDraft && !orgRevision.orgRevisionIsCurrent && !attrOwnership) { + if (Array.isArray(profile.permissionProfiles) && profile.permissionProfiles.length > 0) { + _data.root = profile.permissionProfiles.map((x) => x.orgRootId); + } else { + return new HttpSuccess({ remark: "", data: [] }); + } + } + // กำหนดการเข้าถึงข้อมูลตามสถานะและสิทธิ์ + const isCurrentActive = !orgRevision.orgRevisionIsDraft && orgRevision.orgRevisionIsCurrent; + if (isCurrentActive) { + if (profileAssign && _privilege.privilege !== "OWNER") { + if (_privilege.privilege == "NORMAL") { + const holder = profile.current_holders.find((x) => x.orgRevisionId === id); + if (!holder) return; + _data.root = [holder.orgRootId]; + _data.child1 = [holder.orgChild1Id]; + _data.child2 = [holder.orgChild2Id]; + _data.child3 = [holder.orgChild3Id]; + _data.child4 = [holder.orgChild4Id]; + } else if (_privilege.privilege == "CHILD" || _privilege.privilege == "BROTHER") { + const holder = profile.current_holders.find((x) => x.orgRevisionId === id); + if (!holder) return; + _data.root = [holder.orgRootId]; + if (_privilege.root && _privilege.child1 === null) { + } else if (_privilege.child1 && _privilege.child2 === null) { + _data.child1 = [holder.orgChild1Id]; + } else if (_privilege.child2 && _privilege.child3 === null) { + _data.child1 = [holder.orgChild1Id]; + _data.child2 = [holder.orgChild2Id]; + } else if (_privilege.child3 && _privilege.child4 === null) { + _data.child1 = [holder.orgChild1Id]; + _data.child2 = [holder.orgChild2Id]; + _data.child3 = [holder.orgChild3Id]; + _data.child4 = [holder.orgChild4Id]; + } + } else { + _data.root = [profile.current_holders.find((x) => x.orgRevisionId === id)?.orgRootId]; + } + } else { + if (!attrOwnership) _data = _privilege; + } + } + + const orgRootData = await AppDataSource.getRepository(OrgRoot) .createQueryBuilder("orgRoot") .where("orgRoot.orgRevisionId = :id", { id }) - // .andWhere( - // _data.root != undefined && _data.root != null - // ? _data.root[0] != null - // ? `orgRoot.id IN (:...node)` - // : `orgRoot.id is null` - // : "1=1", - // { - // node: _data.root, - // }, - // ) + .andWhere( + _data.root != undefined && _data.root != null + ? _data.root[0] != null + ? `orgRoot.id IN (:...node)` + : `orgRoot.id is null` + : "1=1", + { + node: _data.root, + }, + ) .leftJoinAndSelect("orgRoot.posMasters", "posMasters") .leftJoinAndSelect("posMasters.current_holder", "current_holder") .orderBy("orgRoot.orgRootOrder", "ASC") @@ -5485,16 +5541,16 @@ export class OrganizationController extends Controller { ? await AppDataSource.getRepository(OrgChild1) .createQueryBuilder("orgChild1") .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - // .andWhere( - // _data.child1 != undefined && _data.child1 != null - // ? _data.child1[0] != null - // ? `orgChild1.id IN (:...node)` - // : `orgChild1.id is null` - // : "1=1", - // { - // node: _data.child1, - // }, - // ) + .andWhere( + _data.child1 != undefined && _data.child1 != null + ? _data.child1[0] != null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is null` + : "1=1", + { + node: _data.child1, + }, + ) .leftJoinAndSelect("orgChild1.posMasters", "posMasters") .leftJoinAndSelect("posMasters.current_holder", "current_holder") .orderBy("orgChild1.orgChild1Order", "ASC") @@ -5507,16 +5563,16 @@ export class OrganizationController extends Controller { ? await AppDataSource.getRepository(OrgChild2) .createQueryBuilder("orgChild2") .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - // .andWhere( - // _data.child2 != undefined && _data.child2 != null - // ? _data.child2[0] != null - // ? `orgChild2.id IN (:...node)` - // : `orgChild2.id is null` - // : "1=1", - // { - // node: _data.child2, - // }, - // ) + .andWhere( + _data.child2 != undefined && _data.child2 != null + ? _data.child2[0] != null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) .leftJoinAndSelect("orgChild2.posMasters", "posMasters") .leftJoinAndSelect("posMasters.current_holder", "current_holder") .orderBy("orgChild2.orgChild2Order", "ASC") @@ -5529,16 +5585,16 @@ export class OrganizationController extends Controller { ? await AppDataSource.getRepository(OrgChild3) .createQueryBuilder("orgChild3") .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - // .andWhere( - // _data.child3 != undefined && _data.child3 != null - // ? _data.child3[0] != null - // ? `orgChild3.id IN (:...node)` - // : `orgChild3.id is null` - // : "1=1", - // { - // node: _data.child3, - // }, - // ) + .andWhere( + _data.child3 != undefined && _data.child3 != null + ? _data.child3[0] != null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) .leftJoinAndSelect("orgChild3.posMasters", "posMasters") .leftJoinAndSelect("posMasters.current_holder", "current_holder") .orderBy("orgChild3.orgChild3Order", "ASC") @@ -5551,16 +5607,16 @@ export class OrganizationController extends Controller { ? await AppDataSource.getRepository(OrgChild4) .createQueryBuilder("orgChild4") .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - // .andWhere( - // _data.child4 != undefined && _data.child4 != null - // ? _data.child4[0] != null - // ? `orgChild4.id IN (:...node)` - // : `orgChild4.id is null` - // : "1=1", - // { - // node: _data.child4, - // }, - // ) + .andWhere( + _data.child4 != undefined && _data.child4 != null + ? _data.child4[0] != null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) .leftJoinAndSelect("orgChild4.posMasters", "posMasters") .leftJoinAndSelect("posMasters.current_holder", "current_holder") .orderBy("orgChild4.orgChild4Order", "ASC") From af466df0d02b0107f834f1ae9285eef689763177 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 5 Feb 2026 16:46:32 +0700 Subject: [PATCH 044/178] fix mode profileSalaryTemp to profileSalary --- .../ProfileSalaryTempController.ts | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 9e458953..6d20d9f0 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1298,8 +1298,8 @@ export class ProfileSalaryTempController extends Controller { const isOfficer = body.type.toUpperCase() === "OFFICER"; /* ========================= - * 1. Load Profile - * ========================= */ + * 1. Load Profile + * ========================= */ const profile = isOfficer ? await queryRunner.manager.findOne(Profile, { where: { id: body.profileId } }) : await queryRunner.manager.findOne(ProfileEmployee, { where: { id: body.profileId } }); @@ -1312,12 +1312,10 @@ export class ProfileSalaryTempController extends Controller { await queryRunner.manager.save(profile); /* ========================= - * 2. Load Salary Temp - * ========================= */ + * 2. Load Salary Temp + * ========================= */ const salaryTemps = await queryRunner.manager.find(ProfileSalaryTemp, { - where: isOfficer - ? { profileId: body.profileId } - : { profileEmployeeId: body.profileId }, + where: isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }, }); if (salaryTemps.length === 0) { @@ -1325,10 +1323,10 @@ export class ProfileSalaryTempController extends Controller { } /* ========================= - * 3. Split Update / Insert - * ========================= */ - const toUpdate = salaryTemps.filter(t => t.salaryId); - const toInsert = salaryTemps.filter(t => !t.salaryId); + * 3. Split Update / Insert + * ========================= */ + // const toUpdate = salaryTemps.filter((t) => t.salaryId && t.isEdit && !t.isDelete); + const toInsert = salaryTemps.filter((t) => !t.isDelete); const dateNow = new Date(); const metaUpdate = { lastUpdateUserId: req.user.sub, @@ -1336,24 +1334,34 @@ export class ProfileSalaryTempController extends Controller { lastUpdatedAt: dateNow, }; - /* ========================= - * 4. UPDATE - * ========================= */ - for (const temp of toUpdate) { - const { id, salaryId, isDelete, isEdit, ...data } = temp; - await queryRunner.manager.update( - ProfileSalary, - { id: salaryId }, - { - ...data, - ...metaUpdate, - }, - ); - } + // delete profile salary temp + await queryRunner.manager.delete(ProfileSalaryTemp, { + ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), + }); + + // delete profile salary + await queryRunner.manager.delete(ProfileSalary, { + ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), + }); /* ========================= - * 5. INSERT (bulk) - * ========================= */ + * 4. UPDATE + * ========================= */ + // for (const temp of toUpdate) { + // const { id, salaryId, isDelete, isEdit, ...data } = temp; + // await queryRunner.manager.update( + // ProfileSalary, + // { id: salaryId }, + // { + // ...data, + // ...metaUpdate, + // }, + // ); + // } + + /* ========================= + * 5. INSERT (bulk) + * ========================= */ if (toInsert.length > 0) { const metaCreate = { createdUserId: req.user.sub, @@ -1371,7 +1379,6 @@ export class ProfileSalaryTempController extends Controller { await queryRunner.commitTransaction(); return new HttpSuccess(); - } catch (error) { await queryRunner.rollbackTransaction(); throw error; @@ -1380,7 +1387,6 @@ export class ProfileSalaryTempController extends Controller { } } - /** * API แก้ไขข้อมูล * From 203ec6cb84e0e95ae99380b2dd28eaa7c7b51fd9 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 5 Feb 2026 17:18:50 +0700 Subject: [PATCH 045/178] fix: run temp to profileSalary --- src/controllers/ProfileSalaryTempController.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 6d20d9f0..95713278 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1326,6 +1326,7 @@ export class ProfileSalaryTempController extends Controller { * 3. Split Update / Insert * ========================= */ // const toUpdate = salaryTemps.filter((t) => t.salaryId && t.isEdit && !t.isDelete); + const backupTemp = salaryTemps; const toInsert = salaryTemps.filter((t) => !t.isDelete); const dateNow = new Date(); const metaUpdate = { @@ -1339,6 +1340,8 @@ export class ProfileSalaryTempController extends Controller { ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), }); + // add insert to profile salary backup + // delete profile salary await queryRunner.manager.delete(ProfileSalary, { ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), @@ -1377,6 +1380,15 @@ export class ProfileSalaryTempController extends Controller { await queryRunner.manager.insert(ProfileSalary, insertData); } + // insert profile salary temp new + if (backupTemp.length > 0) { + const insertBackupTemp = toInsert.map(({ id, ...data }) => ({ + ...data, + })); + + await queryRunner.manager.insert(ProfileSalaryTemp, insertBackupTemp); + } + await queryRunner.commitTransaction(); return new HttpSuccess(); } catch (error) { From 5b726e69c8fe41ba890520f13776949f3a2d2690 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 6 Feb 2026 14:47:13 +0700 Subject: [PATCH 046/178] =?UTF-8?q?Migrate=20add=20table=20profileSalaryBa?= =?UTF-8?q?ckup=20&=20=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=A1=20inse?= =?UTF-8?q?rt=20=E0=B8=A3=E0=B8=B1=E0=B8=81=E0=B8=A9=E0=B8=B2=E0=B8=81?= =?UTF-8?q?=E0=B8=B2=E0=B8=A3=20=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=8A?= =?UTF-8?q?=E0=B9=88=E0=B8=A7=E0=B8=A2=E0=B8=A3=E0=B8=B2=E0=B8=8A=E0=B8=81?= =?UTF-8?q?=E0=B8=B2=E0=B8=A3=20=20#2292?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileSalaryController.ts | 9 +- .../ProfileSalaryEmployeeController.ts | 8 +- .../ProfileSalaryTempController.ts | 101 +++++- src/entities/ProfileSalaryBackup.ts | 295 ++++++++++++++++++ ...291424-create_table_profileSalaryBackup.ts | 14 + 5 files changed, 406 insertions(+), 21 deletions(-) create mode 100644 src/entities/ProfileSalaryBackup.ts create mode 100644 src/migration/1770349291424-create_table_profileSalaryBackup.ts diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index 78975593..4736337a 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -397,8 +397,7 @@ export class ProfileSalaryController extends Controller { const record = await this.salaryRepo.find({ where: { profileId: profile.id, - // commandCode: In(["5", "6", "7"]) - commandCode: In(["5", "6", "7"]), + commandCode: In(["5", "6", "7", "19"]), }, order: { order: "ASC" }, }); @@ -477,7 +476,7 @@ export class ProfileSalaryController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const record = await this.salaryRepo.find({ - where: { profileId: profileId, commandCode: In(["5", "6", "7"]) }, + where: { profileId: profileId, commandCode: In(["5", "6", "7", "19"]) }, order: { order: "ASC" }, }); // const result = record.map((r) => ({ @@ -842,7 +841,7 @@ export class ProfileSalaryController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_SALARY_OFFICER"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_SALARY_OFFICER"); const record = await this.salaryRepo.find({ - where: { profileId: profileId, commandCode: In(["5", "6", "7"]) }, + where: { profileId: profileId, commandCode: In(["5", "6", "7", "19"]) }, order: { order: "ASC" }, }); return new HttpSuccess(record); @@ -909,6 +908,7 @@ export class ProfileSalaryController extends Controller { if (body.commandCode == "7") body.commandName = "เงินพิเศษอื่น ๆ"; else if (body.commandCode == "6") body.commandName = "เลื่อนเงินเดือนกรณีอื่น ๆ"; else if (body.commandCode == "5") body.commandName = "เลื่อนเงินเดือนตามปกติ"; + else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(data, { ...body, ...meta }); const history = new ProfileSalaryHistory(); @@ -1032,6 +1032,7 @@ export class ProfileSalaryController extends Controller { if (body.commandCode == "7") body.commandName = "เงินพิเศษอื่น ๆ"; else if (body.commandCode == "6") body.commandName = "เลื่อนเงินเดือนกรณีอื่น ๆ"; else if (body.commandCode == "5") body.commandName = "เลื่อนเงินเดือนตามปกติ"; + else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(record, body); Object.assign(history, { ...record, id: undefined }); diff --git a/src/controllers/ProfileSalaryEmployeeController.ts b/src/controllers/ProfileSalaryEmployeeController.ts index 36576788..f113a2ba 100644 --- a/src/controllers/ProfileSalaryEmployeeController.ts +++ b/src/controllers/ProfileSalaryEmployeeController.ts @@ -49,7 +49,7 @@ export class ProfileSalaryEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.salaryRepo.find({ - where: { profileEmployeeId: profile.id, commandCode: In(["5", "6", "7"]) }, + where: { profileEmployeeId: profile.id, commandCode: In(["5", "6", "7", "19"]) }, order: { order: "ASC" }, }); return new HttpSuccess(record); @@ -96,7 +96,7 @@ export class ProfileSalaryEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileId); const record = await this.salaryRepo.find({ - where: { profileEmployeeId: profileId, commandCode: In(["5", "6", "7"]) }, + where: { profileEmployeeId: profileId, commandCode: In(["5", "6", "7", "19"]) }, order: { order: "ASC" }, }); return new HttpSuccess(record); @@ -322,7 +322,7 @@ export class ProfileSalaryEmployeeController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_WAGE"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_WAGE"); const record = await this.salaryRepo.find({ - where: { profileEmployeeId: profileId, commandCode: In(["5", "6", "7"]) }, + where: { profileEmployeeId: profileId, commandCode: In(["5", "6", "7", "19"]) }, order: { order: "ASC" }, }); return new HttpSuccess(record); @@ -395,6 +395,7 @@ export class ProfileSalaryEmployeeController extends Controller { if (body.commandCode == "7") body.commandName = "เงินพิเศษอื่น ๆ"; else if (body.commandCode == "6") body.commandName = "เลื่อนเงินเดือนกรณีอื่น ๆ"; else if (body.commandCode == "5") body.commandName = "เลื่อนเงินเดือนตามปกติ"; + else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(data, { ...body, ...meta }); const history = new ProfileSalaryHistory(); @@ -528,6 +529,7 @@ export class ProfileSalaryEmployeeController extends Controller { if (body.commandCode == "7") body.commandName = "เงินพิเศษอื่น ๆ"; else if (body.commandCode == "6") body.commandName = "เลื่อนเงินเดือนกรณีอื่น ๆ"; else if (body.commandCode == "5") body.commandName = "เลื่อนเงินเดือนตามปกติ"; + else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(record, body); Object.assign(history, { ...record, id: undefined }); diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 95713278..b85bc85b 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -35,7 +35,9 @@ import { CreatePositionSalaryEditHistory, PositionSalaryEditHistory, } from "../entities/PositionSalaryEditHistory"; - +import { ProfileSalaryBackup } from "../entities/ProfileSalaryBackup"; +import { ProfileActposition } from "../entities/ProfileActposition"; +import { ProfileAssistance } from "../entities/ProfileAssistance"; @Route("api/v1/org/profile/salaryTemp") @Tags("ProfileSalaryTemp") @Security("bearerAuth") @@ -1328,12 +1330,6 @@ export class ProfileSalaryTempController extends Controller { // const toUpdate = salaryTemps.filter((t) => t.salaryId && t.isEdit && !t.isDelete); const backupTemp = salaryTemps; const toInsert = salaryTemps.filter((t) => !t.isDelete); - const dateNow = new Date(); - const metaUpdate = { - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - lastUpdatedAt: dateNow, - }; // delete profile salary temp await queryRunner.manager.delete(ProfileSalaryTemp, { @@ -1341,6 +1337,23 @@ export class ProfileSalaryTempController extends Controller { }); // add insert to profile salary backup + const profileSalaryBeforeDelete = await queryRunner.manager.find(ProfileSalary, { + where: { + ...(isOfficer + ? { profileId: body.profileId } + : { profileEmployeeId: body.profileId }), + }, + }); + + if (profileSalaryBeforeDelete.length > 0) { + const backupRows = profileSalaryBeforeDelete.map(({ id, ...salary }) => + queryRunner.manager.create(ProfileSalaryBackup, { + ...salary, + profileSalaryId: id, + }) + ); + await queryRunner.manager.insert(ProfileSalaryBackup, backupRows); + } // delete profile salary await queryRunner.manager.delete(ProfileSalary, { @@ -1366,18 +1379,78 @@ export class ProfileSalaryTempController extends Controller { * 5. INSERT (bulk) * ========================= */ if (toInsert.length > 0) { - const metaCreate = { + const dateNow = new Date(); + const metaCreated = { createdUserId: req.user.sub, createdFullName: req.user.name, createdAt: dateNow, + lastUpdateUserId: req.user.sub, + lastUpdateFullName: req.user.name, + lastUpdatedAt: dateNow, }; - const insertData = toInsert.map(({ id, ...data }) => ({ - ...data, - ...metaCreate, - ...metaUpdate, - })); - await queryRunner.manager.insert(ProfileSalary, insertData); + const salaryRows = toInsert.filter( + x => !["17", "18"].includes(x.commandCode) + ); + // ส่งไปรักษาการ + const actPositionRows = toInsert.filter( + x => x.commandCode === "17" + ); + // ส่งไปช่วยราชการ + const assistanceRows = toInsert.filter( + x => x.commandCode === "18" + ); + + if (salaryRows.length) { + await queryRunner.manager.insert( + ProfileSalary, + salaryRows.map(({ id, ...data }) => ({ + ...data, + ...metaCreated, + })) + ); + } + + if (actPositionRows.length) { + await queryRunner.manager.insert( + ProfileActposition, + actPositionRows.map(x => ({ + profileId: x.profileId, + profileEmployeeId: x.profileEmployeeId, + dateStart: x.commandDateAffect, + dateEnd: x.commandDateAffect, + posNo: x.posNo, + position: x.positionName, + commandId: x.commandId, + refCommandNo: x.commandNo, + refCommandDate: x.commandDateAffect, + status: false, + isDeleted: false, + ...metaCreated, + })) + ); + } + + if (assistanceRows.length) { + await queryRunner.manager.insert( + ProfileAssistance, + assistanceRows.map(x => ({ + profileId: x.profileId, + profileEmployeeId: x.profileEmployeeId, + agency: x.orgRoot, + dateStart: x.commandDateAffect, + dateEnd: x.commandDateAffect, + commandId: x.commandId, + commandNo: x.commandNo, + commandName: x.commandName, + refCommandDate: x.commandDateSign, + refId: x.refId, + status: "DONE", + isUpload: false, + ...metaCreated, + })) + ); + } } // insert profile salary temp new diff --git a/src/entities/ProfileSalaryBackup.ts b/src/entities/ProfileSalaryBackup.ts new file mode 100644 index 00000000..29cdd325 --- /dev/null +++ b/src/entities/ProfileSalaryBackup.ts @@ -0,0 +1,295 @@ +import { Entity, Column, Double } from "typeorm"; +import { EntityBase } from "./base/Base"; + +@Entity("profileSalaryBackup") +export class ProfileSalaryBackup extends EntityBase { + + @Column({ + nullable: true, + length: 40, + comment: "อ้างอิง profileSalary (no FK constraint)", + type: "uuid", + default: null, + }) + profileSalaryId: string; + + @Column({ + nullable: true, + length: 40, + comment: "อ้างอิง profile (no FK constraint)", + type: "uuid", + default: null, + }) + profileId: string; + + @Column({ + nullable: true, + length: 40, + comment: "อ้างอิง ProfileEmployee (no FK constraint)", + default: null, + }) + profileEmployeeId: string; + + @Column({ + nullable: true, + comment: "เรียงลำดับใหมาตามการนำเข้า", + default: null, + }) + order: number; + + @Column({ + nullable: true, + comment: "เลขที่คำสั่ง", + default: null, + }) + commandNo: string; + + @Column({ + nullable: true, + comment: "ปีที่ออกคำสั่ง", + default: null, + }) + commandYear: number; + + @Column({ + comment: "คำสั่งวันที่", + type: "datetime", + nullable: true, + }) + commandDateSign: Date; + + @Column({ + comment: "คำสั่งมีผลวันที่", + type: "datetime", + nullable: true, + }) + commandDateAffect: Date; + + @Column({ + nullable: true, + comment: "รหัสประเภทของคำสั่ง", + default: null, + }) + commandCode: string; + + @Column({ + nullable: true, + comment: "ชื่อประเภทคำสั่ง", + default: null, + }) + commandName: string; + + @Column({ + nullable: true, + length: 40, + comment: "ตัวย่อเลขที่ตำแหน่ง", + default: null, + }) + posNoAbb: string; + + @Column({ + nullable: true, + length: 40, + comment: "เลขที่ตำแหน่ง", + default: null, + }) + posNo: string; + + @Column({ + nullable: true, + length: 255, + comment: "ตำแหน่ง", + default: null, + }) + positionName: string; + + @Column({ + nullable: true, + length: 255, + comment: "ประเภทตำแหน่ง", + default: null, + }) + positionType: string; + + @Column({ + nullable: true, + length: 255, + comment: "ระดับตำแหน่ง", + default: null, + }) + positionLevel: string; + + @Column({ + nullable: true, + comment: "ระดับของเก่าที่ยังไม่เทียบเท่าแบบแท่ง", + default: null, + }) + positionCee: string; + + @Column({ + nullable: true, + comment: "root name", + default: null, + }) + orgRoot: string; + + @Column({ + nullable: true, + comment: "child1 name", + default: null, + }) + orgChild1: string; + + @Column({ + nullable: true, + comment: "child2 name", + default: null, + }) + orgChild2: string; + + @Column({ + nullable: true, + comment: "child3 name", + default: null, + }) + orgChild3: string; + + @Column({ + nullable: true, + comment: "child4 name", + default: null, + }) + orgChild4: string; + + @Column({ + nullable: true, + length: 255, + comment: "ตำแหน่งทางการบริหาร", + default: null, + }) + positionExecutive: string; + + @Column({ + comment: "เงินเดือนฐาน", + default: 0, + nullable: true, + type: "double", + }) + amount: Double; + + @Column({ + comment: "เงินพิเศษ", + default: 0, + nullable: true, + type: "double", + }) + amountSpecial: Double; + + @Column({ + comment: "เงินประจำตำแหน่ง", + default: 0, + nullable: true, + type: "double", + }) + positionSalaryAmount: Double; + + @Column({ + comment: "เงินค่าตอบแทนรายเดือน", + default: 0, + nullable: true, + type: "double", + }) + mouthSalaryAmount: Double; + + @Column({ + nullable: true, + type: "text", + comment: "หมายเหตุ", + default: null, + }) + remark: string; + + @Column({ + nullable: true, + comment: "refId", + default: null, + }) + refId: string; + + @Column({ + comment: "วันที่", + type: "datetime", + nullable: true, + }) + dateGovernment: Date; + + @Column({ + nullable: true, + comment: "เข้ารับราชการ", + default: null, + }) + isGovernment: boolean; + + @Column({ + comment: "ข้อมูลจาก Entry", + default: false, + }) + isEntry: boolean; + + @Column({ + nullable: true, + length: 255, + comment: "ด้านของตำแหน่ง", + default: null, + }) + positionPathSide: string; + + @Column({ + nullable: true, + length: 255, + comment: "ตำแหน่งในสายงาน", + default: null, + }) + positionLine: string; + + @Column({ + nullable: true, + length: 40, + comment: "อ้างอิง command (no FK constraint)", + default: null, + }) + commandId: string; + + @Column({ + nullable: true, + length: 255, + comment: "หน่วยงานที่ออกคำสั่ง", + default: null, + }) + posNumCodeSit: string; + + @Column({ + nullable: true, + length: 255, + comment: "หน่วยงานที่ออกคำสั่ง(ตัวย่อ)", + default: null, + }) + posNumCodeSitAbb: string; + + @Column({ + nullable: true, + length: 255, + comment: "ด้านทางการบริหาร", + default: null, + }) + positionExecutiveField: string; + + @Column({ + nullable: true, + length: 255, + comment: "ด้าน/สาขา", + default: null, + }) + positionArea: string; + +} \ No newline at end of file diff --git a/src/migration/1770349291424-create_table_profileSalaryBackup.ts b/src/migration/1770349291424-create_table_profileSalaryBackup.ts new file mode 100644 index 00000000..2f5c5b8d --- /dev/null +++ b/src/migration/1770349291424-create_table_profileSalaryBackup.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateTableProfileSalaryBackup1770349291424 implements MigrationInterface { + name = 'CreateTableProfileSalaryBackup1770349291424' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE \`profileSalaryBackup\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileSalaryId\` varchar(40) NULL COMMENT 'อ้างอิง profileSalary (no FK constraint)', \`profileId\` varchar(40) NULL COMMENT 'อ้างอิง profile (no FK constraint)', \`profileEmployeeId\` varchar(40) NULL COMMENT 'อ้างอิง ProfileEmployee (no FK constraint)', \`order\` int NULL COMMENT 'เรียงลำดับใหมาตามการนำเข้า', \`commandNo\` varchar(255) NULL COMMENT 'เลขที่คำสั่ง', \`commandYear\` int NULL COMMENT 'ปีที่ออกคำสั่ง', \`commandDateSign\` datetime NULL COMMENT 'คำสั่งวันที่', \`commandDateAffect\` datetime NULL COMMENT 'คำสั่งมีผลวันที่', \`commandCode\` varchar(255) NULL COMMENT 'รหัสประเภทของคำสั่ง', \`commandName\` varchar(255) NULL COMMENT 'ชื่อประเภทคำสั่ง', \`posNoAbb\` varchar(40) NULL COMMENT 'ตัวย่อเลขที่ตำแหน่ง', \`posNo\` varchar(40) NULL COMMENT 'เลขที่ตำแหน่ง', \`positionName\` varchar(255) NULL COMMENT 'ตำแหน่ง', \`positionType\` varchar(255) NULL COMMENT 'ประเภทตำแหน่ง', \`positionLevel\` varchar(255) NULL COMMENT 'ระดับตำแหน่ง', \`positionCee\` varchar(255) NULL COMMENT 'ระดับของเก่าที่ยังไม่เทียบเท่าแบบแท่ง', \`orgRoot\` varchar(255) NULL COMMENT 'root name', \`orgChild1\` varchar(255) NULL COMMENT 'child1 name', \`orgChild2\` varchar(255) NULL COMMENT 'child2 name', \`orgChild3\` varchar(255) NULL COMMENT 'child3 name', \`orgChild4\` varchar(255) NULL COMMENT 'child4 name', \`positionExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร', \`amount\` double NULL COMMENT 'เงินเดือนฐาน' DEFAULT '0', \`amountSpecial\` double NULL COMMENT 'เงินพิเศษ' DEFAULT '0', \`positionSalaryAmount\` double NULL COMMENT 'เงินประจำตำแหน่ง' DEFAULT '0', \`mouthSalaryAmount\` double NULL COMMENT 'เงินค่าตอบแทนรายเดือน' DEFAULT '0', \`remark\` text NULL COMMENT 'หมายเหตุ', \`refId\` varchar(255) NULL COMMENT 'refId', \`dateGovernment\` datetime NULL COMMENT 'วันที่', \`isGovernment\` tinyint NULL COMMENT 'เข้ารับราชการ', \`isEntry\` tinyint NOT NULL COMMENT 'ข้อมูลจาก Entry' DEFAULT 0, \`positionPathSide\` varchar(255) NULL COMMENT 'ด้านของตำแหน่ง', \`positionLine\` varchar(255) NULL COMMENT 'ตำแหน่งในสายงาน', \`commandId\` varchar(40) NULL COMMENT 'อ้างอิง command (no FK constraint)', \`posNumCodeSit\` varchar(255) NULL COMMENT 'หน่วยงานที่ออกคำสั่ง', \`posNumCodeSitAbb\` varchar(255) NULL COMMENT 'หน่วยงานที่ออกคำสั่ง(ตัวย่อ)', \`positionExecutiveField\` varchar(255) NULL COMMENT 'ด้านทางการบริหาร', \`positionArea\` varchar(255) NULL COMMENT 'ด้าน/สาขา', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE \`profileSalaryBackup\``); + } + +} From 1696890f74c23044778451439572b1b9b68480ab Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 6 Feb 2026 14:47:47 +0700 Subject: [PATCH 047/178] =?UTF-8?q?=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=B9=E0=B8=A5?= =?UTF-8?q?=20"=E0=B8=AD=E0=B8=B2=E0=B8=A2=E0=B8=B8=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=8A=E0=B8=81=E0=B8=B2=E0=B8=A3=20(=E0=B8=81=E0=B8=97?= =?UTF-8?q?=E0=B8=A1.)"=20#2285?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileGovernmentController.ts | 3 +++ src/controllers/ProfileGovernmentEmployeeController.ts | 3 +++ src/interfaces/utils.ts | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProfileGovernmentController.ts b/src/controllers/ProfileGovernmentController.ts index f2208036..dcb138df 100644 --- a/src/controllers/ProfileGovernmentController.ts +++ b/src/controllers/ProfileGovernmentController.ts @@ -127,6 +127,7 @@ export class ProfileGovernmentHistoryController extends Controller { dateRetireLaw: record.dateRetireLaw ?? null, // govAge: record.dateStart == null ? null : calculateAge(record.dateStart), govAge: await calculateGovAge(profile.id, "OFFICER"), + govAgeBkk: await calculateGovAge(profile.id, "OFFICER", true), dateAppoint: record.dateAppoint, dateStart: record.dateStart, govAgeAbsent: record.govAgeAbsent, @@ -310,6 +311,7 @@ export class ProfileGovernmentHistoryController extends Controller { dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), govAge: await calculateGovAge(profileId, "OFFICER"), + govAgeBkk: await calculateGovAge(profileId, "OFFICER", true), dateAppoint: record?.dateAppoint, dateStart: record?.dateStart, govAgeAbsent: record?.govAgeAbsent, @@ -483,6 +485,7 @@ export class ProfileGovernmentHistoryController extends Controller { dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), govAge: await calculateGovAge(profileId, "OFFICER"), + govAgeBkk: await calculateGovAge(profileId, "OFFICER", true), dateAppoint: record?.dateAppoint, dateStart: record?.dateStart, govAgeAbsent: record?.govAgeAbsent, diff --git a/src/controllers/ProfileGovernmentEmployeeController.ts b/src/controllers/ProfileGovernmentEmployeeController.ts index c44b3583..6709e5db 100644 --- a/src/controllers/ProfileGovernmentEmployeeController.ts +++ b/src/controllers/ProfileGovernmentEmployeeController.ts @@ -121,6 +121,7 @@ export class ProfileGovernmentEmployeeController extends Controller { dateRetireLaw: record.dateRetireLaw ?? null, // govAge: record.dateStart == null ? null : calculateAge(record.dateStart), govAge: await calculateGovAge(profile.id, "EMPLOYEE"), + govAgeBkk: await calculateGovAge(profile.id, "EMPLOYEE", true), dateAppoint: record.dateAppoint, dateStart: record.dateStart, govAgeAbsent: record.govAgeAbsent, @@ -292,6 +293,7 @@ export class ProfileGovernmentEmployeeController extends Controller { dateRetire: record?.dateRetire ?? null, //วันครบเกษียณอายุ // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), //อายุราชการ govAge: await calculateGovAge(profileEmployeeId, "EMPLOYEE"), + govAgeBkk: await calculateGovAge(profileEmployeeId, "EMPLOYEE", true), govAgeAbsent: record?.govAgeAbsent ?? null, // ขาดราชการ govAgePlus: record?.govAgePlus, // อายุราชการเกื้อกูล dateRetireLaw: record?.dateRetireLaw ?? null, // วันที่เกษียฯอายุราชการตามกฎหมาย @@ -451,6 +453,7 @@ export class ProfileGovernmentEmployeeController extends Controller { dateRetire: record?.dateRetire ?? null, //วันครบเกษียณอายุ // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), //อายุราชการ govAge: await calculateGovAge(profileEmployeeId, "EMPLOYEE"), + govAgeBkk: await calculateGovAge(profileEmployeeId, "EMPLOYEE", true), govAgeAbsent: record?.govAgeAbsent ?? null, // ขาดราชการ govAgePlus: record?.govAgePlus, // อายุราชการเกื้อกูล dateRetireLaw: record?.dateRetireLaw ?? null, // วันที่เกษียฯอายุราชการตามกฎหมาย diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts index abe4bd4a..e572f038 100644 --- a/src/interfaces/utils.ts +++ b/src/interfaces/utils.ts @@ -44,7 +44,7 @@ export function calculateAge(start: Date, end = new Date()) { return { year, month, day }; } -export async function calculateGovAge(profileId: string, type: string) { +export async function calculateGovAge(profileId: string, type: string, bkk?: boolean) { // type = OFFICER , EMPLOYEE const isEmployee = type === "EMPLOYEE"; @@ -107,7 +107,9 @@ export async function calculateGovAge(profileId: string, type: string) { }); } // const firstStartDate = new Date(records[0].date); - const firstStartDate = profile?.dateAppoint ? profile?.dateAppoint : new Date(); + const firstStartDate = !bkk + ? profile?.dateAppoint ? profile?.dateAppoint : new Date() + : profile?.dateStart ? profile?.dateStart : new Date() ; const firstEndDate = endDateFristRec ? new Date(endDateFristRec.dateGovernment) : new Date(); // console.log("firstStartDate1", firstStartDate); From 8a649086f72388a6add3538e31e1b48fe654a5a7 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 6 Feb 2026 14:55:59 +0700 Subject: [PATCH 048/178] fix: #2239 --- src/controllers/OrganizationController.ts | 252 +++++++++++++--------- src/interfaces/permission.ts | 16 ++ src/interfaces/utils.ts | 138 +++++++----- 3 files changed, 242 insertions(+), 164 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8799d8cd..c3d8e04b 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -33,7 +33,7 @@ import { PosMaster } from "../entities/PosMaster"; import { Profile } from "../entities/Profile"; import { RequestWithUser } from "../middlewares/user"; import permission from "../interfaces/permission"; -import { checkQueueInProgress, setLogDataDiff } from "../interfaces/utils"; +import { checkQueueInProgress, resolveNodeId, resolveNodeLevel, setLogDataDiff } from "../interfaces/utils"; import { sendToQueueOrg, sendToQueueOrgDraft } from "../services/rabbitmq"; import { PosType } from "../entities/PosType"; import { PosLevel } from "../entities/PosLevel"; @@ -5516,6 +5516,8 @@ export class OrganizationController extends Controller { } } + const orgDna = await new permission().checkDna(request, request.user.sub) + let level: any = resolveNodeLevel(orgDna); const orgRootData = await AppDataSource.getRepository(OrgRoot) .createQueryBuilder("orgRoot") @@ -5623,6 +5625,36 @@ export class OrganizationController extends Controller { .getMany() : []; + const cannotViewRootPosMaster = + (_privilege.privilege === "PARENT") || + (_privilege.privilege === "BROTHER" && level > 1) || + (_privilege.privilege === "CHILD" && level != 0) || + (_privilege.privilege === "NORMAL" && level != 0); + + const cannotViewChild1PosMaster = + (_privilege.privilege === "PARENT" && level > 1) || + (_privilege.privilege === "BROTHER" && level > 2) || + (_privilege.privilege === "CHILD" && level !== 1) || + (_privilege.privilege === "NORMAL" && level !== 1); + + const cannotViewChild2PosMaster = + (_privilege.privilege === "PARENT" && level > 2) || + (_privilege.privilege === "BROTHER" && level > 3) || + (_privilege.privilege === "CHILD" && level !== 2) || + (_privilege.privilege === "NORMAL" && level !== 2); + + const cannotViewChild3PosMaster = + (_privilege.privilege === "PARENT" && level > 3) || + (_privilege.privilege === "BROTHER" && level > 4) || + (_privilege.privilege === "CHILD" && level !== 3) || + (_privilege.privilege === "NORMAL" && level !== 3); + + const cannotViewChild4PosMaster = + (_privilege.privilege === "PARENT" && level > 4) || + (_privilege.privilege === "CHILD" && level !== 4) || + (_privilege.privilege === "NORMAL" && level !== 4); + + // const formattedData = orgRootData.map((orgRoot) => { const formattedData = await Promise.all( orgRootData.map(async (orgRoot) => { @@ -5637,27 +5669,29 @@ export class OrganizationController extends Controller { orgRootName: orgRoot.orgRootName, labelName: orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, - posMaster: await Promise.all( - orgRoot.posMasters - .filter( - (x) => - x.orgChild1Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgRoot.orgRootShortName} ${x.posMasterNo}`, - orgTreeId: orgRoot.id, - orgLevel: 0, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), + posMaster: + cannotViewRootPosMaster ? [] : + await Promise.all( + orgRoot.posMasters + .filter( + (x) => + x.orgChild1Id == null && + // x.current_holderId != null && + x.isDirector === true, + ) + // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC + // .slice(0, 3) // Select the first 3 rows + .map(async (x) => ({ + posmasterId: x.id, + posNo: `${orgRoot.orgRootShortName} ${x.posMasterNo}`, + orgTreeId: orgRoot.id, + orgLevel: 0, + fullNameCurrentHolder: + x.current_holder == null + ? null + : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, + })), + ), children: await Promise.all( orgChild1Data .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) @@ -5685,27 +5719,29 @@ export class OrganizationController extends Controller { "00" + " " + orgRoot.orgRootShortName, - posMaster: await Promise.all( - orgChild1.posMasters - .filter( - (x) => - x.orgChild2Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild1.orgChild1ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild1.id, - orgLevel: 1, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), + posMaster: + cannotViewChild1PosMaster ? [] : + await Promise.all( + orgChild1.posMasters + .filter( + (x) => + x.orgChild2Id == null && + // x.current_holderId != null && + x.isDirector === true, + ) + // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC + // .slice(0, 3) // Select the first 3 rows + .map(async (x) => ({ + posmasterId: x.id, + posNo: `${orgChild1.orgChild1ShortName} ${x.posMasterNo}`, + orgTreeId: orgChild1.id, + orgLevel: 1, + fullNameCurrentHolder: + x.current_holder == null + ? null + : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, + })), + ), children: await Promise.all( orgChild2Data @@ -5741,27 +5777,29 @@ export class OrganizationController extends Controller { "00" + " " + orgRoot.orgRootShortName, - posMaster: await Promise.all( - orgChild2.posMasters - .filter( - (x) => - x.orgChild3Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild2.orgChild2ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild2.id, - orgLevel: 2, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), + posMaster: + cannotViewChild2PosMaster ? [] : + await Promise.all( + orgChild2.posMasters + .filter( + (x) => + x.orgChild3Id == null && + // x.current_holderId != null && + x.isDirector === true, + ) + // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC + // .slice(0, 3) // Select the first 3 rows + .map(async (x) => ({ + posmasterId: x.id, + posNo: `${orgChild2.orgChild2ShortName} ${x.posMasterNo}`, + orgTreeId: orgChild2.id, + orgLevel: 2, + fullNameCurrentHolder: + x.current_holder == null + ? null + : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, + })), + ), children: await Promise.all( orgChild3Data @@ -5804,27 +5842,29 @@ export class OrganizationController extends Controller { "00" + " " + orgRoot.orgRootShortName, - posMaster: await Promise.all( - orgChild3.posMasters - .filter( - (x) => - x.orgChild4Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild3.orgChild3ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild3.id, - orgLevel: 3, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), + posMaster: + cannotViewChild3PosMaster ? [] : + await Promise.all( + orgChild3.posMasters + .filter( + (x) => + x.orgChild4Id == null && + // x.current_holderId != null && + x.isDirector === true, + ) + // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC + // .slice(0, 3) // Select the first 3 rows + .map(async (x) => ({ + posmasterId: x.id, + posNo: `${orgChild3.orgChild3ShortName} ${x.posMasterNo}`, + orgTreeId: orgChild3.id, + orgLevel: 3, + fullNameCurrentHolder: + x.current_holder == null + ? null + : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, + })), + ), children: await Promise.all( orgChild4Data @@ -5874,26 +5914,28 @@ export class OrganizationController extends Controller { "00" + " " + orgRoot.orgRootShortName, - posMaster: await Promise.all( - orgChild4.posMasters - .filter( - (x) => - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild4.orgChild4ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild4.id, - orgLevel: 4, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), + posMaster: + cannotViewChild4PosMaster ? [] : + await Promise.all( + orgChild4.posMasters + .filter( + (x) => + // x.current_holderId != null && + x.isDirector === true, + ) + // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC + // .slice(0, 3) // Select the first 3 rows + .map(async (x) => ({ + posmasterId: x.id, + posNo: `${orgChild4.orgChild4ShortName} ${x.posMasterNo}`, + orgTreeId: orgChild4.id, + orgLevel: 4, + fullNameCurrentHolder: + x.current_holder == null + ? null + : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, + })), + ), })), ), })), diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index 1542ce45..fa61df3d 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -305,6 +305,22 @@ class CheckAuth { public async PermissionOrgUserUpdate(req: RequestWithUser, system: string, profileId: string) { return await this.PermissionOrgByUser(req, system, "UPDATE", profileId); } + + public async checkDna(request: RequestWithUser, keycloakId: any) { + try { + const result = await new CallAPI().GetData( + request, + `/org/finddna-by-keycloak/${keycloakId}`, + false + ); + + return result; + } catch (error) { + console.error("Error calling API:", error); + throw error; + } + } + } export default CheckAuth; diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts index e572f038..d3409187 100644 --- a/src/interfaces/utils.ts +++ b/src/interfaces/utils.ts @@ -72,7 +72,7 @@ export async function calculateGovAge(profileId: string, type: string, bkk?: boo }); // console.log("endDateFristRec", endDateFristRec.dateGovernment); - const calculateDuration = (startDate: any, endDate: any , inclusive = false) => { + const calculateDuration = (startDate: any, endDate: any, inclusive = false) => { if (inclusive) { endDate = new Date(endDate.getTime() + 86400000); // +1 วัน } @@ -107,9 +107,9 @@ export async function calculateGovAge(profileId: string, type: string, bkk?: boo }); } // const firstStartDate = new Date(records[0].date); - const firstStartDate = !bkk - ? profile?.dateAppoint ? profile?.dateAppoint : new Date() - : profile?.dateStart ? profile?.dateStart : new Date() ; + const firstStartDate = !bkk + ? profile?.dateAppoint ? profile?.dateAppoint : new Date() + : profile?.dateStart ? profile?.dateStart : new Date(); const firstEndDate = endDateFristRec ? new Date(endDateFristRec.dateGovernment) : new Date(); // console.log("firstStartDate1", firstStartDate); @@ -143,37 +143,37 @@ export async function calculateGovAge(profileId: string, type: string, bkk?: boo for (let i = 0; i < records_middle.length; i++) { const current = records_middle[i]; const next = records_middle[i + 1]; - + // นับเฉพาะช่วงที่เป็นราชการ if (current.isGovernment === true) { const startDate = new Date(current.dateGovernment); const endDate = next ? new Date(next.dateGovernment) : new Date(); const { years, months, days } = calculateDuration(startDate, endDate); - + totalYears2 += years; totalMonths2 += months; totalDays2 += days; - + // console.log(`✔ นับช่วง ${startDate.toISOString()} → ${endDate.toISOString()} ได้ ${years} ปี ${months} เดือน ${days} วัน`); - } + } // else { // console.log(`❌ ไม่รวมช่วง ${current.dateGovernment} เพราะ isGovernment = false`); // } } - + //ตั้งแต่วันที่กลับมารับราขการไปจนถึงรวมถึงวันที่ปัจจุบัน - const adjustTotal = (years:number, months:number, days:number) => { + const adjustTotal = (years: number, months: number, days: number) => { const daysInThisMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate(); if (days >= daysInThisMonth) { months += Math.floor(days / daysInThisMonth); days %= daysInThisMonth; } - + if (months >= 12) { years += Math.floor(months / 12); months %= 12; } - + return { years, months, days }; }; @@ -192,10 +192,10 @@ export async function calculateGovAge(profileId: string, type: string, bkk?: boo const finalAdjusted = adjustTotal(sumYears, sumMonths, sumDays); return { - year: finalAdjusted.years, - month: finalAdjusted.months, - day: finalAdjusted.days, -}; + year: finalAdjusted.years, + month: finalAdjusted.months, + day: finalAdjusted.days, + }; } export function calculateRetireDate(birthDate: Date) { @@ -262,11 +262,11 @@ export async function removeProfileInOrganize(profileId: string, type: string) { .getOne(); const draftRevision = await AppDataSource.getRepository(OrgRevision) - .createQueryBuilder("orgRevision") - .where("orgRevision.orgRevisionIsDraft = true") - .andWhere("orgRevision.orgRevisionIsCurrent = false") - .getOne(); - + .createQueryBuilder("orgRevision") + .where("orgRevision.orgRevisionIsDraft = true") + .andWhere("orgRevision.orgRevisionIsCurrent = false") + .getOne(); + if (!currentRevision && !draftRevision) { return; } @@ -354,31 +354,31 @@ export async function removeProfileInOrganize(profileId: string, type: string) { } export async function removePostMasterAct(profileId: string) { - const currentRevision = await AppDataSource.getRepository(OrgRevision) - .createQueryBuilder("orgRevision") - .where("orgRevision.orgRevisionIsDraft = false") - .andWhere("orgRevision.orgRevisionIsCurrent = true") - .getOne(); + const currentRevision = await AppDataSource.getRepository(OrgRevision) + .createQueryBuilder("orgRevision") + .where("orgRevision.orgRevisionIsDraft = false") + .andWhere("orgRevision.orgRevisionIsCurrent = true") + .getOne(); - if (!currentRevision) { - return; - } + if (!currentRevision) { + return; + } - const findProfileInposMaster = await AppDataSource.getRepository(PosMaster) - .createQueryBuilder("posMaster") - .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id }) - .andWhere("posMaster.current_holderId = :profileId", { profileId }) - .getOne(); + const findProfileInposMaster = await AppDataSource.getRepository(PosMaster) + .createQueryBuilder("posMaster") + .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id }) + .andWhere("posMaster.current_holderId = :profileId", { profileId }) + .getOne(); - if (!findProfileInposMaster) { - return; - } + if (!findProfileInposMaster) { + return; + } - const posMasterAct = await AppDataSource.getRepository(PosMasterAct) - .createQueryBuilder("posMasterAct") - .where("posMasterAct.posMasterChildId = :posMasterChildId", { posMasterChildId: findProfileInposMaster.id }) - .getMany(); - await AppDataSource.getRepository(PosMasterAct).remove(posMasterAct); + const posMasterAct = await AppDataSource.getRepository(PosMasterAct) + .createQueryBuilder("posMasterAct") + .where("posMasterAct.posMasterChildId = :posMasterChildId", { posMasterChildId: findProfileInposMaster.id }) + .getMany(); + await AppDataSource.getRepository(PosMasterAct).remove(posMasterAct); } export async function checkReturnCommandType(commandId: string) { @@ -567,22 +567,22 @@ export function editLogSequence(req: RequestWithUser, index: number, data: LogSe export async function checkQueueInProgress(queueName: string) { const axios = require('axios'); - // console.log("Checking queue in progress"); - const res = await axios.get(`${process.env.RABBIT_API_URL}/api/queues/%2F/${queueName}`, { - auth: { username: process.env.RABBIT_USER , password: process.env.RABBIT_PASS }, - }); + // console.log("Checking queue in progress"); + const res = await axios.get(`${process.env.RABBIT_API_URL}/api/queues/%2F/${queueName}`, { + auth: { username: process.env.RABBIT_USER, password: process.env.RABBIT_PASS }, + }); - const q = res.data; + const q = res.data; - // console.log(`Queue "${queueName}" has:`); - // console.log(` - ${q.messages_ready} messages ready`); - // console.log(` - ${q.messages_unacknowledged} messages in progress (unacked)`); + // console.log(`Queue "${queueName}" has:`); + // console.log(` - ${q.messages_ready} messages ready`); + // console.log(` - ${q.messages_unacknowledged} messages in progress (unacked)`); - if (q.messages_unacknowledged > 0) { - return true; - } + if (q.messages_unacknowledged > 0) { + return true; + } - return false; + return false; } export function chunkArray(array: any, size: number) { @@ -594,7 +594,7 @@ export function chunkArray(array: any, size: number) { } export async function PayloadSendNoti(commandId: string) { - if (!commandId) + if (!commandId) return ""; const commandRepository = AppDataSource.getRepository(Command); const _command = await commandRepository.findOne({ @@ -606,8 +606,8 @@ export async function PayloadSendNoti(commandId: string) { if (!_command || !_command.commandType) return ""; const _payload = { - name: _command && _command.commandType - ? `คำสั่ง${_command.commandType.name}` + name: _command && _command.commandType + ? `คำสั่ง${_command.commandType.name}` : "", url: `${process.env.API_URL}/salary/file/ระบบออกคำสั่ง/คำสั่ง/${commandId}/คำสั่ง`, isReport: true, @@ -616,8 +616,8 @@ export async function PayloadSendNoti(commandId: string) { let attachments = {} if (_command.commandType.isUploadAttachment === true) { const _payloadAtt = { - name: _command && _command.commandType - ? `เอกสารแนบท้ายคำสั่ง${_command.commandType.name}` + name: _command && _command.commandType + ? `เอกสารแนบท้ายคำสั่ง${_command.commandType.name}` : "", url: `${process.env.API_URL}/salary/file/ระบบออกคำสั่ง/แนบท้าย/${commandId}/แนบท้าย`, isReport: true, @@ -733,3 +733,23 @@ export function commandTypePath(commandCode: string): string | null { return null; } } + +export function resolveNodeLevel(data: any) { + if (data.child4DnaId) return 4; + if (data.child3DnaId) return 3; + if (data.child2DnaId) return 2; + if (data.child1DnaId) return 1; + if (data.rootDnaId) return 0; + return null; +} + +export function resolveNodeId(data: any) { + return ( + data.child4DnaId ?? + data.child3DnaId ?? + data.child2DnaId ?? + data.child1DnaId ?? + data.rootDnaId ?? + null + ); +} \ No newline at end of file From 77b545d392e6a01c3195f1dc4ad019428771f372 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 6 Feb 2026 15:12:52 +0700 Subject: [PATCH 049/178] fix child privilege --- src/controllers/OrganizationController.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index c3d8e04b..767056a9 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -5628,30 +5628,30 @@ export class OrganizationController extends Controller { const cannotViewRootPosMaster = (_privilege.privilege === "PARENT") || (_privilege.privilege === "BROTHER" && level > 1) || - (_privilege.privilege === "CHILD" && level != 0) || + (_privilege.privilege === "CHILD" && level >= 0) || (_privilege.privilege === "NORMAL" && level != 0); const cannotViewChild1PosMaster = (_privilege.privilege === "PARENT" && level > 1) || (_privilege.privilege === "BROTHER" && level > 2) || - (_privilege.privilege === "CHILD" && level !== 1) || + (_privilege.privilege === "CHILD" && level >= 1) || (_privilege.privilege === "NORMAL" && level !== 1); const cannotViewChild2PosMaster = (_privilege.privilege === "PARENT" && level > 2) || (_privilege.privilege === "BROTHER" && level > 3) || - (_privilege.privilege === "CHILD" && level !== 2) || + (_privilege.privilege === "CHILD" && level >= 2) || (_privilege.privilege === "NORMAL" && level !== 2); const cannotViewChild3PosMaster = (_privilege.privilege === "PARENT" && level > 3) || (_privilege.privilege === "BROTHER" && level > 4) || - (_privilege.privilege === "CHILD" && level !== 3) || + (_privilege.privilege === "CHILD" && level >= 3) || (_privilege.privilege === "NORMAL" && level !== 3); const cannotViewChild4PosMaster = (_privilege.privilege === "PARENT" && level > 4) || - (_privilege.privilege === "CHILD" && level !== 4) || + (_privilege.privilege === "CHILD" && level >= 4) || (_privilege.privilege === "NORMAL" && level !== 4); From 256296672d7f955304899c885bef79b5b482bea0 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 6 Feb 2026 15:25:54 +0700 Subject: [PATCH 050/178] privilege --- src/controllers/OrganizationController.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 767056a9..e9e4dc5d 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -5628,30 +5628,30 @@ export class OrganizationController extends Controller { const cannotViewRootPosMaster = (_privilege.privilege === "PARENT") || (_privilege.privilege === "BROTHER" && level > 1) || - (_privilege.privilege === "CHILD" && level >= 0) || + (_privilege.privilege === "CHILD" && level > 0) || (_privilege.privilege === "NORMAL" && level != 0); const cannotViewChild1PosMaster = (_privilege.privilege === "PARENT" && level > 1) || (_privilege.privilege === "BROTHER" && level > 2) || - (_privilege.privilege === "CHILD" && level >= 1) || + (_privilege.privilege === "CHILD" && level > 1) || (_privilege.privilege === "NORMAL" && level !== 1); const cannotViewChild2PosMaster = (_privilege.privilege === "PARENT" && level > 2) || (_privilege.privilege === "BROTHER" && level > 3) || - (_privilege.privilege === "CHILD" && level >= 2) || + (_privilege.privilege === "CHILD" && level > 2) || (_privilege.privilege === "NORMAL" && level !== 2); const cannotViewChild3PosMaster = (_privilege.privilege === "PARENT" && level > 3) || (_privilege.privilege === "BROTHER" && level > 4) || - (_privilege.privilege === "CHILD" && level >= 3) || + (_privilege.privilege === "CHILD" && level > 3) || (_privilege.privilege === "NORMAL" && level !== 3); const cannotViewChild4PosMaster = (_privilege.privilege === "PARENT" && level > 4) || - (_privilege.privilege === "CHILD" && level >= 4) || + (_privilege.privilege === "CHILD" && level > 4) || (_privilege.privilege === "NORMAL" && level !== 4); From 528f8f75c12fd491c4f2fb7ec69f4780e62c2966 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 6 Feb 2026 16:50:31 +0700 Subject: [PATCH 051/178] add query --- src/controllers/OrganizationController.ts | 1581 +++++++++++---------- 1 file changed, 807 insertions(+), 774 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8799d8cd..3d9871a4 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -287,16 +287,13 @@ export class OrganizationController extends Controller { * */ @Get("finddna-by-keycloak/{keycloakId}") - async FindDnaOrgByKeycloakId( - @Path() keycloakId: string - ) { - + async FindDnaOrgByKeycloakId(@Path() keycloakId: string) { let reply: any; const profileByKeycloak: any = await this.profileRepo.findOne({ where: { keycloak: keycloakId }, - select: ["id", "keycloak"] - }) + select: ["id", "keycloak"], + }); const orgRevision = await this.orgRevisionRepository.findOne({ select: ["id"], @@ -450,157 +447,157 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - // .andWhere( - // _data.child1 != undefined && _data.child1 != null - // ? _data.child1[0] != null - // ? `orgChild1.id IN (:...node)` - // : `orgChild1.id is null` - // : "1=1", - // { - // node: _data.child1, - // }, - // ) - .select([ - "orgChild1.id", - "orgChild1.isOfficer", - "orgChild1.isInformation", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + // .andWhere( + // _data.child1 != undefined && _data.child1 != null + // ? _data.child1[0] != null + // ? `orgChild1.id IN (:...node)` + // : `orgChild1.id is null` + // : "1=1", + // { + // node: _data.child1, + // }, + // ) + .select([ + "orgChild1.id", + "orgChild1.isOfficer", + "orgChild1.isInformation", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - // .andWhere( - // _data.child2 != undefined && _data.child2 != null - // ? _data.child2[0] != null - // ? `orgChild2.id IN (:...node)` - // : `orgChild2.id is null` - // : "1=1", - // { - // node: _data.child2, - // }, - // ) - .select([ - "orgChild2.id", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + // .andWhere( + // _data.child2 != undefined && _data.child2 != null + // ? _data.child2[0] != null + // ? `orgChild2.id IN (:...node)` + // : `orgChild2.id is null` + // : "1=1", + // { + // node: _data.child2, + // }, + // ) + .select([ + "orgChild2.id", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - // .andWhere( - // _data.child3 != undefined && _data.child3 != null - // ? _data.child3[0] != null - // ? `orgChild3.id IN (:...node)` - // : `orgChild3.id is null` - // : "1=1", - // { - // node: _data.child3, - // }, - // ) - .select([ - "orgChild3.id", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + // .andWhere( + // _data.child3 != undefined && _data.child3 != null + // ? _data.child3[0] != null + // ? `orgChild3.id IN (:...node)` + // : `orgChild3.id is null` + // : "1=1", + // { + // node: _data.child3, + // }, + // ) + .select([ + "orgChild3.id", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - // .andWhere( - // _data.child4 != undefined && _data.child4 != null - // ? _data.child4[0] != null - // ? `orgChild4.id IN (:...node)` - // : `orgChild4.id is null` - // : "1=1", - // { - // node: _data.child4, - // }, - // ) - .select([ - "orgChild4.id", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + // .andWhere( + // _data.child4 != undefined && _data.child4 != null + // ? _data.child4[0] != null + // ? `orgChild4.id IN (:...node)` + // : `orgChild4.id is null` + // : "1=1", + // { + // node: _data.child4, + // }, + // ) + .select([ + "orgChild4.id", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; const formattedData = await Promise.all( @@ -1253,13 +1250,13 @@ export class OrganizationController extends Controller { where: orgRevision.orgRevisionIsCurrent && !orgRevision.orgRevisionIsDraft ? { - orgRevisionId: id, - current_holderId: profile.id, - } + orgRevisionId: id, + current_holderId: profile.id, + } : { - orgRevisionId: id, - next_holderId: profile.id, - }, + orgRevisionId: id, + next_holderId: profile.id, + }, }); if (!posMaster) return new HttpSuccess([]); @@ -1736,161 +1733,161 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 !== undefined && _data.child1 !== null - ? _data.child1[0] !== null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is null` - : "1=1", - { - node: _data.child1, - }, - ) - .select([ - "orgChild1.id", - "orgChild1.misId", - "orgChild1.isOfficer", - "orgChild1.isInformation", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 !== undefined && _data.child1 !== null + ? _data.child1[0] !== null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is null` + : "1=1", + { + node: _data.child1, + }, + ) + .select([ + "orgChild1.id", + "orgChild1.misId", + "orgChild1.isOfficer", + "orgChild1.isInformation", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 !== undefined && _data.child2 !== null - ? _data.child2[0] !== null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .select([ - "orgChild2.id", - "orgChild2.misId", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 !== undefined && _data.child2 !== null + ? _data.child2[0] !== null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .select([ + "orgChild2.id", + "orgChild2.misId", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 !== undefined && _data.child3 !== null - ? _data.child3[0] !== null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .select([ - "orgChild3.id", - "orgChild3.misId", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 !== undefined && _data.child3 !== null + ? _data.child3[0] !== null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .select([ + "orgChild3.id", + "orgChild3.misId", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 !== undefined && _data.child4 !== null - ? _data.child4[0] !== null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .select([ - "orgChild4.id", - "orgChild4.misId", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 !== undefined && _data.child4 !== null + ? _data.child4[0] !== null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .select([ + "orgChild4.id", + "orgChild4.misId", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; const formattedData = await Promise.all( @@ -3442,18 +3439,18 @@ export class OrganizationController extends Controller { const formattedData_ = rootId === "root" ? [ - { - personID: "", - name: "", - avatar: "", - positionName: "", - positionNum: "", - positionNumInt: null, - departmentName: data.orgRevisionName, - organizationId: data.id, - children: formattedData, - }, - ] + { + personID: "", + name: "", + avatar: "", + positionName: "", + positionNum: "", + positionNumInt: null, + departmentName: data.orgRevisionName, + organizationId: data.id, + children: formattedData, + }, + ] : formattedData; return new HttpSuccess(formattedData_); } @@ -3493,17 +3490,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3531,19 +3528,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3576,21 +3573,21 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3626,23 +3623,23 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -3681,25 +3678,25 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: // await this.posMasterRepository.count({ // where: { @@ -3744,27 +3741,27 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgRootId: orgRoot.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: // await this.posMasterRepository.count({ // where: { @@ -3829,17 +3826,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -3867,19 +3864,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -3912,21 +3909,21 @@ export class OrganizationController extends Controller { totalPositionCurrentVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -3962,23 +3959,23 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRootId: data.id, @@ -4017,25 +4014,25 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRootId: data.id, + orgChild1Id: orgChild1.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: // await this.posMasterRepository.count({ // where: { @@ -4093,17 +4090,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild1Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild1Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild1Id: data.id, @@ -4131,19 +4128,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild1Id: data.id, + orgChild2Id: orgChild2.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild1Id: data.id, + orgChild2Id: orgChild2.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild1Id: data.id, @@ -4176,21 +4173,21 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -4226,23 +4223,23 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgRevisionId: data.id, + orgChild2Id: orgChild2.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgRevisionId: data.id, @@ -4290,17 +4287,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild2Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild2Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild2Id: data.id, @@ -4328,19 +4325,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild2Id: data.id, @@ -4373,21 +4370,21 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild2Id: data.id, + orgChild3Id: orgChild3.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild2Id: data.id, @@ -4431,17 +4428,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild3Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild3Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild3Id: data.id, @@ -4469,19 +4466,19 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild3Id: data.id, + orgChild4Id: orgChild4.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild3Id: data.id, + orgChild4Id: orgChild4.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild3Id: data.id, @@ -4521,17 +4518,17 @@ export class OrganizationController extends Controller { totalPositionVacant: data.orgRevision.orgRevisionIsDraft == true ? await this.posMasterRepository.count({ - where: { - orgChild4Id: data.id, - next_holderId: IsNull() || "", - }, - }) + where: { + orgChild4Id: data.id, + next_holderId: IsNull() || "", + }, + }) : await this.posMasterRepository.count({ - where: { - orgChild4Id: data.id, - current_holderId: IsNull() || "", - }, - }), + where: { + orgChild4Id: data.id, + current_holderId: IsNull() || "", + }, + }), // totalPositionCurrentVacant: await this.posMasterRepository.count({ // where: { // orgChild4Id: data.id, @@ -5516,7 +5513,6 @@ export class OrganizationController extends Controller { } } - const orgRootData = await AppDataSource.getRepository(OrgRoot) .createQueryBuilder("orgRoot") .where("orgRoot.orgRevisionId = :id", { id }) @@ -5539,88 +5535,88 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 != undefined && _data.child1 != null - ? _data.child1[0] != null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is null` - : "1=1", - { - node: _data.child1, - }, - ) - .leftJoinAndSelect("orgChild1.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 != undefined && _data.child1 != null + ? _data.child1[0] != null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is null` + : "1=1", + { + node: _data.child1, + }, + ) + .leftJoinAndSelect("orgChild1.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .leftJoinAndSelect("orgChild2.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 != undefined && _data.child2 != null + ? _data.child2[0] != null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .leftJoinAndSelect("orgChild2.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .leftJoinAndSelect("orgChild3.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 != undefined && _data.child3 != null + ? _data.child3[0] != null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .leftJoinAndSelect("orgChild3.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .leftJoinAndSelect("orgChild4.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 != undefined && _data.child4 != null + ? _data.child4[0] != null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .leftJoinAndSelect("orgChild4.posMasters", "posMasters") + .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; // const formattedData = orgRootData.map((orgRoot) => { @@ -6070,159 +6066,159 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 != undefined && _data.child1 != null - ? _data.child1[0] != null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : "1=1", - { - node: _data.child1, - }, - ) - .select([ - "orgChild1.id", - "orgChild1.ancestorDNA", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 != undefined && _data.child1 != null + ? _data.child1[0] != null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : "1=1", + { + node: _data.child1, + }, + ) + .select([ + "orgChild1.id", + "orgChild1.ancestorDNA", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .select([ - "orgChild2.id", - "orgChild2.ancestorDNA", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 != undefined && _data.child2 != null + ? _data.child2[0] != null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .select([ + "orgChild2.id", + "orgChild2.ancestorDNA", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .select([ - "orgChild3.id", - "orgChild3.ancestorDNA", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 != undefined && _data.child3 != null + ? _data.child3[0] != null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .select([ + "orgChild3.id", + "orgChild3.ancestorDNA", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .select([ - "orgChild4.id", - "orgChild4.ancestorDNA", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 != undefined && _data.child4 != null + ? _data.child4[0] != null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .select([ + "orgChild4.id", + "orgChild4.ancestorDNA", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; // const formattedData = orgRootData.map((orgRoot) => { @@ -7761,4 +7757,41 @@ export class OrganizationController extends Controller { const total = profiles.length; return new HttpSuccess({ total, successAmount: check }); } + + /** + * API ย้ายโครงสร้างแบบร่างไปโครงสร้างปัจจุบัน โดยอ้างอิงตาม rootId + * + * @summary - ย้ายโครงสร้างและตำแหน่งจากแบบร่างไปโครงสร้างปัจจุบัน โดยอ้างอิงตาม rootId + * + */ + @Post("move-draft-to-current/{rootId}") + async moveDraftToCurrent(@Request() request: RequestWithUser) { + // part 1 ข้อมูลโครงสร้าง + const drafRevision = await this.orgRevisionRepository.findOne({ + where: { + orgRevisionIsDraft: true, + orgRevisionIsCurrent: false, + }, + select: ["id"], + }); + + const currentRevision = await this.orgRevisionRepository.findOne({ + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + select: ["id"], + }); + + if (!drafRevision || !currentRevision) + return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล"); + + const drafRevisionId = drafRevision.id; + const currentRevisionId = currentRevision.id; + + // start transaction multi table orgRoot, orgChild1, orgChild2, orgChild3, orgChild4, posMaster, position + + // part 2 ข้อมูลตำแหน่ง + // 1. ดึงข้อมูลคนออกจากโครงสร้างปัจจุบัน + } } From 9aa0a97a531babf191a7e7030cd4f23bfd33854f Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 6 Feb 2026 17:02:08 +0700 Subject: [PATCH 052/178] fix script --- .../ProfileSalaryTempController.ts | 37 ++++++++----------- src/entities/ProfileSalaryTemp.ts | 5 ++- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index b85bc85b..40c0003c 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1339,9 +1339,7 @@ export class ProfileSalaryTempController extends Controller { // add insert to profile salary backup const profileSalaryBeforeDelete = await queryRunner.manager.find(ProfileSalary, { where: { - ...(isOfficer - ? { profileId: body.profileId } - : { profileEmployeeId: body.profileId }), + ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), }, }); @@ -1350,7 +1348,7 @@ export class ProfileSalaryTempController extends Controller { queryRunner.manager.create(ProfileSalaryBackup, { ...salary, profileSalaryId: id, - }) + }), ); await queryRunner.manager.insert(ProfileSalaryBackup, backupRows); } @@ -1389,17 +1387,11 @@ export class ProfileSalaryTempController extends Controller { lastUpdatedAt: dateNow, }; - const salaryRows = toInsert.filter( - x => !["17", "18"].includes(x.commandCode) - ); - // ส่งไปรักษาการ - const actPositionRows = toInsert.filter( - x => x.commandCode === "17" - ); + const salaryRows = toInsert.filter((x) => !["17", "18"].includes(x.commandCode)); + // ส่งไปรักษาการ + const actPositionRows = toInsert.filter((x) => x.commandCode === "17"); // ส่งไปช่วยราชการ - const assistanceRows = toInsert.filter( - x => x.commandCode === "18" - ); + const assistanceRows = toInsert.filter((x) => x.commandCode === "18"); if (salaryRows.length) { await queryRunner.manager.insert( @@ -1407,14 +1399,14 @@ export class ProfileSalaryTempController extends Controller { salaryRows.map(({ id, ...data }) => ({ ...data, ...metaCreated, - })) + })), ); } - if (actPositionRows.length) { + if (actPositionRows.length > 0) { await queryRunner.manager.insert( ProfileActposition, - actPositionRows.map(x => ({ + actPositionRows.map((x) => ({ profileId: x.profileId, profileEmployeeId: x.profileEmployeeId, dateStart: x.commandDateAffect, @@ -1427,14 +1419,14 @@ export class ProfileSalaryTempController extends Controller { status: false, isDeleted: false, ...metaCreated, - })) + })), ); } - - if (assistanceRows.length) { + + if (assistanceRows.length > 0) { await queryRunner.manager.insert( ProfileAssistance, - assistanceRows.map(x => ({ + assistanceRows.map((x) => ({ profileId: x.profileId, profileEmployeeId: x.profileEmployeeId, agency: x.orgRoot, @@ -1448,7 +1440,7 @@ export class ProfileSalaryTempController extends Controller { status: "DONE", isUpload: false, ...metaCreated, - })) + })), ); } } @@ -1457,6 +1449,7 @@ export class ProfileSalaryTempController extends Controller { if (backupTemp.length > 0) { const insertBackupTemp = toInsert.map(({ id, ...data }) => ({ ...data, + salaryId: null, })); await queryRunner.manager.insert(ProfileSalaryTemp, insertBackupTemp); diff --git a/src/entities/ProfileSalaryTemp.ts b/src/entities/ProfileSalaryTemp.ts index 9b13d071..97e74ec7 100644 --- a/src/entities/ProfileSalaryTemp.ts +++ b/src/entities/ProfileSalaryTemp.ts @@ -273,8 +273,9 @@ export class ProfileSalaryTemp extends EntityBase { length: 40, comment: "คีย์นอก(FK)ของตาราง profileSalary", default: null, + type: "varchar", }) - salaryId: string; + salaryId: string | null; @Column({ nullable: true, @@ -292,7 +293,7 @@ export class ProfileSalaryTemp extends EntityBase { }) posNumCodeSitAbb: string; - @Column({ + @Column({ nullable: true, length: 255, comment: "ด้านทางการบริหาร", From 2627c58244a9ee2b59a62c20e41679e484a6b54a Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 6 Feb 2026 21:47:56 +0700 Subject: [PATCH 053/178] fix script move positionSalaryTemp to positionSalary --- .../ProfileSalaryTempController.ts | 121 +++++++++++------- 1 file changed, 76 insertions(+), 45 deletions(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 40c0003c..85834c79 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -38,6 +38,17 @@ import { import { ProfileSalaryBackup } from "../entities/ProfileSalaryBackup"; import { ProfileActposition } from "../entities/ProfileActposition"; import { ProfileAssistance } from "../entities/ProfileAssistance"; + +const SALARY_COMMAND_CODES = { + ACT_POSITION: "17", // รักษาการ + ASSISTANCE: "18", // ช่วยราชการ +} as const; + +class ConfirmDoneSalaryDto { + profileId: string; + type: "OFFICER" | "EMPLOYEE"; +} + @Route("api/v1/org/profile/salaryTemp") @Tags("ProfileSalaryTemp") @Security("bearerAuth") @@ -1013,7 +1024,10 @@ export class ProfileSalaryTempController extends Controller { if (!salary) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } - return new HttpSuccess({ salaryNew: salary, salaryOld: salary.profileSalary }); + return new HttpSuccess({ + salaryNew: salary, + salaryOld: salary.profileSalary, + }); } /** @@ -1242,7 +1256,9 @@ export class ProfileSalaryTempController extends Controller { profile.statusCheckEdit = "PENDING"; await this.profileRepo.save(profile); } else { - const profile = await this.profileEmployeeRepo.findOneBy({ id: body.profileId }); + const profile = await this.profileEmployeeRepo.findOneBy({ + id: body.profileId, + }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } @@ -1271,7 +1287,9 @@ export class ProfileSalaryTempController extends Controller { profile.statusCheckEdit = "EDITED"; await this.profileRepo.save(profile); } else { - const profile = await this.profileEmployeeRepo.findOneBy({ id: body.profileId }); + const profile = await this.profileEmployeeRepo.findOneBy({ + id: body.profileId, + }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } @@ -1290,7 +1308,7 @@ export class ProfileSalaryTempController extends Controller { @Post("confirm-done") public async confirmDoneSalary( @Request() req: RequestWithUser, - @Body() body: { profileId: string; type: string }, + @Body() body: ConfirmDoneSalaryDto, ) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); @@ -1303,8 +1321,12 @@ export class ProfileSalaryTempController extends Controller { * 1. Load Profile * ========================= */ const profile = isOfficer - ? await queryRunner.manager.findOne(Profile, { where: { id: body.profileId } }) - : await queryRunner.manager.findOne(ProfileEmployee, { where: { id: body.profileId } }); + ? await queryRunner.manager.findOne(Profile, { + where: { id: body.profileId }, + }) + : await queryRunner.manager.findOne(ProfileEmployee, { + where: { id: body.profileId }, + }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบข้อมูล profile"); @@ -1325,24 +1347,26 @@ export class ProfileSalaryTempController extends Controller { } /* ========================= - * 3. Split Update / Insert + * 3. สำรองข้อมูล Temp ไว้ restore ภายหลัง * ========================= */ - // const toUpdate = salaryTemps.filter((t) => t.salaryId && t.isEdit && !t.isDelete); - const backupTemp = salaryTemps; + const originalTempRows = salaryTemps; // เก็บไว้ restore หลัง insert ProfileSalary เสร็จ const toInsert = salaryTemps.filter((t) => !t.isDelete); - // delete profile salary temp + /* ========================= + * 4. ลบข้อมูล ProfileSalaryTemp และ ProfileSalary ปัจจุบัน + * ========================= */ await queryRunner.manager.delete(ProfileSalaryTemp, { ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), }); - // add insert to profile salary backup + // backup profile salary before delete const profileSalaryBeforeDelete = await queryRunner.manager.find(ProfileSalary, { where: { ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), }, }); + // insert ProfileSalary backup to ProfileSalaryBackup if (profileSalaryBeforeDelete.length > 0) { const backupRows = profileSalaryBeforeDelete.map(({ id, ...salary }) => queryRunner.manager.create(ProfileSalaryBackup, { @@ -1358,21 +1382,6 @@ export class ProfileSalaryTempController extends Controller { ...(isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId }), }); - /* ========================= - * 4. UPDATE - * ========================= */ - // for (const temp of toUpdate) { - // const { id, salaryId, isDelete, isEdit, ...data } = temp; - // await queryRunner.manager.update( - // ProfileSalary, - // { id: salaryId }, - // { - // ...data, - // ...metaUpdate, - // }, - // ); - // } - /* ========================= * 5. INSERT (bulk) * ========================= */ @@ -1387,12 +1396,22 @@ export class ProfileSalaryTempController extends Controller { lastUpdatedAt: dateNow, }; - const salaryRows = toInsert.filter((x) => !["17", "18"].includes(x.commandCode)); - // ส่งไปรักษาการ - const actPositionRows = toInsert.filter((x) => x.commandCode === "17"); - // ส่งไปช่วยราชการ - const assistanceRows = toInsert.filter((x) => x.commandCode === "18"); + // แยกข้อมูลตามประเภทการ insert + const { salaryRows, actPositionRows, assistanceRows } = toInsert.reduce<{ + salaryRows: ProfileSalaryTemp[]; + actPositionRows: ProfileSalaryTemp[]; + assistanceRows: ProfileSalaryTemp[]; + }>( + (acc, x) => { + if (x.commandCode === SALARY_COMMAND_CODES.ACT_POSITION) acc.actPositionRows.push(x); + else if (x.commandCode === SALARY_COMMAND_CODES.ASSISTANCE) acc.assistanceRows.push(x); + else acc.salaryRows.push(x); + return acc; + }, + { salaryRows: [], actPositionRows: [], assistanceRows: [] }, + ); + // Insert ProfileSalary if (salaryRows.length) { await queryRunner.manager.insert( ProfileSalary, @@ -1403,7 +1422,8 @@ export class ProfileSalaryTempController extends Controller { ); } - if (actPositionRows.length > 0) { + // Insert ProfileActposition + if (actPositionRows.length) { await queryRunner.manager.insert( ProfileActposition, actPositionRows.map((x) => ({ @@ -1423,7 +1443,8 @@ export class ProfileSalaryTempController extends Controller { ); } - if (assistanceRows.length > 0) { + // Insert ProfileAssistance + if (assistanceRows.length) { await queryRunner.manager.insert( ProfileAssistance, assistanceRows.map((x) => ({ @@ -1445,14 +1466,14 @@ export class ProfileSalaryTempController extends Controller { } } - // insert profile salary temp new - if (backupTemp.length > 0) { - const insertBackupTemp = toInsert.map(({ id, ...data }) => ({ - ...data, - salaryId: null, - })); - - await queryRunner.manager.insert(ProfileSalaryTemp, insertBackupTemp); + /* ========================= + * Restore ProfileSalaryTemp (หลังจาก ProfileSalary insert เรียบร้อยแล้ว) + * ========================= */ + if (originalTempRows.length > 0) { + await queryRunner.manager.insert( + ProfileSalaryTemp, + originalTempRows.map(({ id, ...data }) => data), + ); } await queryRunner.commitTransaction(); @@ -1547,7 +1568,9 @@ export class ProfileSalaryTempController extends Controller { salaryId: string, @Request() req: RequestWithUser, ) { - const source_item = await this.salaryRepo.findOne({ where: { id: salaryId } }); + const source_item = await this.salaryRepo.findOne({ + where: { id: salaryId }, + }); // if (source_item) { //await new permission().PermissionOrgUserGet(req,"SYS_REGISTRY_OFFICER",source_item.profileId,); //ไม่แน่ใจOFFปิดไว้ก่อน // } @@ -1555,7 +1578,10 @@ export class ProfileSalaryTempController extends Controller { const sourceOrder = source_item.order; if (direction.trim().toUpperCase() == "UP") { const dest_item = await this.salaryRepo.findOne({ - where: { profileId: source_item.profileId, order: LessThan(sourceOrder) }, + where: { + profileId: source_item.profileId, + order: LessThan(sourceOrder), + }, order: { order: "DESC" }, }); if (dest_item == null) return new HttpSuccess(); @@ -1567,7 +1593,10 @@ export class ProfileSalaryTempController extends Controller { await Promise.all([this.salaryRepo.save(source_item), this.salaryRepo.save(dest_item)]); } else { const dest_item = await this.salaryRepo.findOne({ - where: { profileId: source_item.profileId, order: MoreThan(sourceOrder) }, + where: { + profileId: source_item.profileId, + order: MoreThan(sourceOrder), + }, order: { order: "ASC" }, }); if (dest_item == null) return new HttpSuccess(); @@ -1598,7 +1627,9 @@ export class ProfileSalaryTempController extends Controller { profile = await this.profileRepo.findOneBy({ id: profileId }); if (!profile) { - profileEmployee = await this.profileEmployeeRepo.findOneBy({ id: profileId }); + profileEmployee = await this.profileEmployeeRepo.findOneBy({ + id: profileId, + }); if (!profileEmployee) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); } From e750b3963952cda981125c5ba3c7e445e433c2d0 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 6 Feb 2026 21:58:45 +0700 Subject: [PATCH 054/178] fix salaryId set null --- src/controllers/ProfileSalaryTempController.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 85834c79..d0458f15 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1472,7 +1472,10 @@ export class ProfileSalaryTempController extends Controller { if (originalTempRows.length > 0) { await queryRunner.manager.insert( ProfileSalaryTemp, - originalTempRows.map(({ id, ...data }) => data), + originalTempRows.map(({ id, ...data }) => ({ + ...data, + salaryId: null, + })), ); } From 922a0ab1c276f24e1242ca105ffab0e7eab1b2f3 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 6 Feb 2026 22:11:07 +0700 Subject: [PATCH 055/178] =?UTF-8?q?fix:=20=E0=B8=8A=E0=B9=88=E0=B8=A7?= =?UTF-8?q?=E0=B8=A2=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=81=E0=B8=B2=E0=B8=A3?= =?UTF-8?q?=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=A3=E0=B8=B1=E0=B8=81=E0=B8=A9?= =?UTF-8?q?=E0=B8=B2=E0=B8=81=E0=B8=B2=E0=B8=A3=20endDate=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileSalaryTempController.ts | 4 ++-- src/entities/ProfileActposition.ts | 2 +- src/entities/ProfileAssistance.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index d0458f15..6ea1713c 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1430,7 +1430,7 @@ export class ProfileSalaryTempController extends Controller { profileId: x.profileId, profileEmployeeId: x.profileEmployeeId, dateStart: x.commandDateAffect, - dateEnd: x.commandDateAffect, + dateEnd: null, posNo: x.posNo, position: x.positionName, commandId: x.commandId, @@ -1452,7 +1452,7 @@ export class ProfileSalaryTempController extends Controller { profileEmployeeId: x.profileEmployeeId, agency: x.orgRoot, dateStart: x.commandDateAffect, - dateEnd: x.commandDateAffect, + dateEnd: null, commandId: x.commandId, commandNo: x.commandNo, commandName: x.commandName, diff --git a/src/entities/ProfileActposition.ts b/src/entities/ProfileActposition.ts index 4d20c0d8..bde07e30 100644 --- a/src/entities/ProfileActposition.ts +++ b/src/entities/ProfileActposition.ts @@ -29,7 +29,7 @@ export class ProfileActposition extends EntityBase { comment: "วันที่สิ้นสุด", default: null, }) - dateEnd: Date; + dateEnd: Date | null; @Column({ nullable: true, diff --git a/src/entities/ProfileAssistance.ts b/src/entities/ProfileAssistance.ts index 568552c0..ad01f585 100644 --- a/src/entities/ProfileAssistance.ts +++ b/src/entities/ProfileAssistance.ts @@ -43,7 +43,7 @@ export class ProfileAssistance extends EntityBase { comment: "วันสิ้นสุดการช่วยราชการ", default: null, }) - dateEnd: Date; + dateEnd: Date | null; @Column({ nullable: true, From f2ab1ec91e79635021adc28f1284883cf15699db Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 9 Feb 2026 11:13:15 +0700 Subject: [PATCH 056/178] add fields --- src/controllers/CommandController.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 0c5a8653..5c801315 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -519,6 +519,7 @@ export class CommandController extends Controller { commandTypeName: command.commandType?.name || null, commandCode: command.commandType?.code || null, commandSysId: command.commandType?.commandSysId || null, + createdUserId: command.createdUserId }; return new HttpSuccess(_command); } @@ -2310,6 +2311,19 @@ export class CommandController extends Controller { } } if (issue == null) issue = "..................................."; + + const operators = await this.commandOperatorRepository.find({ + select: { + prefix: true, + firstName: true, + lastName: true, + roleName: true, + orderNo: true + }, + where: { commandId: command.id }, + order: { orderNo: "ASC" }, + }); + return new HttpSuccess({ template: command.commandType.fileAttachment, reportName: "xlsx-report", @@ -2325,6 +2339,15 @@ export class CommandController extends Controller { command.commandExcecuteDate == null ? "" : Extension.ToThaiNumber(Extension.ToThaiFullDate2(command.commandExcecuteDate)), + operators: operators.length > 0 + ? operators.map(x => ({ + fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, + roleName: x.roleName + })) + : [{ + fullName: "", + roleName: "เจ้าหน้าที่ดำเนินการ" + }] }, }); } From 638362df1c50c297f276e21995b4ec78ec00613c Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 9 Feb 2026 12:35:59 +0700 Subject: [PATCH 057/178] feat: improve move-draft-to-current with differential sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement differential sync for organization structure and positions instead of delete-all-and-insert-all approach. Changes: - Add OrgIdMapping and AllOrgMappings interfaces for tracking ID mappings - Implement syncOrgLevel() helper for differential sync per org level - Add syncPositionsForPosMaster() helper for position table sync - Process org levels bottom-up (Child4→Child3→Child2→Child1→Root) - Use ancestorDNA matching with Like operator for descendant sync - Cascade delete positions before deleting org nodes - Batch DELETE/UPDATE/INSERT operations for better performance - Track draft→current ID mappings for position updates - Add comprehensive documentation in docs/move-draft-to-current.md Benefits: - Preserve IDs for unchanged nodes (better tracking) - More efficient (fewer database operations) - Better data integrity with proper FK handling - Sync all descendants under given rootDnaId Co-Authored-By: Claude Opus 4.6 --- docs/move-draft-to-current.md | 356 +++++++++++++ src/controllers/OrganizationController.ts | 582 +++++++++++++++++++++- src/interfaces/OrgMapping.ts | 24 + 3 files changed, 938 insertions(+), 24 deletions(-) create mode 100644 docs/move-draft-to-current.md create mode 100644 src/interfaces/OrgMapping.ts diff --git a/docs/move-draft-to-current.md b/docs/move-draft-to-current.md new file mode 100644 index 00000000..320c784e --- /dev/null +++ b/docs/move-draft-to-current.md @@ -0,0 +1,356 @@ +# Move Draft to Current - Differential Sync Implementation + +## Overview + +This document describes the implementation of the improved `move-draft-to-current` function in `OrganizationController.ts`. The function synchronizes organization structure and position data from the **Draft Revision** to the **Current Revision** using a differential sync approach (instead of the previous "delete all and insert all" method). + +**API Endpoint:** `POST /api/v1/org/move-draft-to-current/{rootDnaId}` + +--- + +## Architecture + +### Data Models + +The organization structure consists of 5 hierarchical levels: + +``` +OrgRoot (Level 0) + └── OrgChild1 (Level 1) + └── OrgChild2 (Level 2) + └── OrgChild3 (Level 3) + └── OrgChild4 (Level 4) +``` + +Each level has: +- Organization nodes with `ancestorDNA` for hierarchical tracking +- Foreign key relationships to parent levels +- Associated position records (`PosMaster`) + +### Type Definitions + +Located in `src/interfaces/OrgMapping.ts`: + +```typescript +interface OrgIdMapping { + byAncestorDNA: Map; // ancestorDNA → current ID + byDraftId: Map; // draft ID → current ID +} + +interface AllOrgMappings { + orgRoot: OrgIdMapping; + orgChild1: OrgIdMapping; + orgChild2: OrgIdMapping; + orgChild3: OrgIdMapping; + orgChild4: OrgIdMapping; +} +``` + +--- + +## Implementation Workflow + +### Phase 0: Preparation + +1. **Get Revision IDs** + - Fetch Draft Revision (`orgRevisionIsDraft: true`) + - Fetch Current Revision (`orgRevisionIsCurrent: true`) + +2. **Validate rootDnaId** + - Check if rootDnaId exists in Draft Revision + - Return error if not found + +### Phase 1: Sync Organization Structure (Bottom-Up) + +**Processing Order:** `OrgChild4 → OrgChild3 → OrgChild2 → OrgChild1 → OrgRoot` + +**Why Bottom-Up?** Child nodes have no dependent children (only parent references), allowing safe deletion without FK violations. + +#### For Each Organization Level + +The `syncOrgLevel()` helper performs: + +1. **FETCH** - Get all draft and current nodes under `rootDnaId` + ```typescript + where: { ancestorDNA: Like(`${rootDnaId}%`) } // All descendants + ``` + +2. **DELETE** - Remove current nodes not in draft + - Cascade delete positions first (via `cascadeDeletePositions()`) + - Delete the organization node + +3. **UPDATE** - Update nodes that exist in both (matched by `ancestorDNA`) + - Map parent IDs using `parentMappings` + - Preserve original node ID + +4. **INSERT** - Add draft nodes not in current + - Create new node with mapped parent IDs + - Return new ID for tracking + +5. **RETURN** - Return `OrgIdMapping` for next level + +**Result:** `allMappings` contains draft ID → current ID mappings for all org levels + +### Phase 2: Sync Position Data + +#### Step 2.1: Clear current_holderId + +```typescript +// Clear holders for positions that will have new holders +await queryRunner.manager.update(PosMaster, + { current_holderId: In(nextHolderIds) }, + { current_holderId: null, isSit: false } +) +``` + +#### Step 2.2: Fetch Draft and Current Positions + +- Get draft positions using `draftOrgIds` from `allMappings` +- Get current positions using `currentOrgIds` from `allMappings` + +#### Step 2.3: Batch DELETE + +```typescript +// Delete current positions not in draft (cascade delete positions first) +await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }) +await queryRunner.manager.delete(PosMaster, toDeleteIds) +``` + +#### Step 2.4: Process UPDATE or INSERT + +For each draft position: +1. Map organization IDs using `resolveOrgId()` +2. If exists in current → **UPDATE** +3. If not exists → **INSERT** +4. Track `draftPosMasterId → currentPosMasterId` mapping + +#### Step 2.5: Sync Position Table + +For each mapped PosMaster: +```typescript +await syncPositionsForPosMaster( + queryRunner, + draftPosMasterId, + currentPosMasterId, + draftRevisionId, + currentRevisionId +) +``` + +--- + +## Helper Functions + +### `resolveOrgId(draftId, mapping)` + +Maps a draft organization ID to its current ID. + +```typescript +private resolveOrgId( + draftId: string | null, + mapping: OrgIdMapping +): string | null { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; +} +``` + +### `cascadeDeletePositions(queryRunner, node, entityClass)` + +Deletes positions associated with an organization node before deleting the node itself. + +```typescript +private async cascadeDeletePositions( + queryRunner: any, + node: any, + entityClass: any +): Promise { + const whereClause = { orgRevisionId: node.orgRevisionId }; + + // Set FK field based on entity type + if (entityClass === OrgRoot) whereClause.orgRootId = node.id; + else if (entityClass === OrgChild1) whereClause.orgChild1Id = node.id; + // ... etc + + await queryRunner.manager.delete(PosMaster, whereClause); +} +``` + +### `syncOrgLevel(...)` + +Generic differential sync for each organization level. + +**Parameters:** +- `queryRunner` - Database query runner +- `entityClass` - Organization entity class (OrgRoot, OrgChild1, etc.) +- `repository` - Repository for the entity +- `draftRevisionId` - Draft revision ID +- `currentRevisionId` - Current revision ID +- `rootDnaId` - Root DNA ID to sync under +- `parentMappings` - Mappings from child levels (for FK resolution) + +**Returns:** `OrgIdMapping` for this level + +### `syncPositionsForPosMaster(...)` + +Syncs positions for a PosMaster record. + +**Parameters:** +- `queryRunner` - Database query runner +- `draftPosMasterId` - Draft PosMaster ID +- `currentPosMasterId` - Current PosMaster ID +- `draftRevisionId` - Draft revision ID +- `currentRevisionId` - Current revision ID + +**Process:** +1. Fetch draft and current positions +2. Delete current positions not in draft (by `orderNo`) +3. Update existing positions +4. Insert new positions + +--- + +## Transaction Management + +All operations are wrapped in a transaction: + +```typescript +const queryRunner = AppDataSource.createQueryRunner(); +await queryRunner.connect(); +await queryRunner.startTransaction(); + +try { + // ... all sync operations + await queryRunner.commitTransaction(); +} catch (error) { + await queryRunner.rollbackTransaction(); + throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "..."); +} +``` + +--- + +## Benefits of Differential Sync + +| Aspect | Old Approach (Delete All + Insert) | New Approach (Differential Sync) | +|--------|-----------------------------------|----------------------------------| +| **ID Preservation** | All IDs changed | Unchanged nodes keep original IDs | +| **Performance** | N deletes + N inserts | Only changed data processed | +| **Tracking** | Cannot track what changed | Can track additions/updates/deletes | +| **Data Integrity** | Higher risk of data loss | Better integrity with cascade deletes | +| **Scalability** | Poor for large datasets | Efficient with batch operations | + +--- + +## Database Schema Relationships + +``` +orgRevision + ├── orgRoot (FK: orgRevisionId) + │ ├── orgChild1 (FK: orgRootId, orgRevisionId) + │ │ ├── orgChild2 (FK: orgChild1Id, orgRootId, orgRevisionId) + │ │ │ ├── orgChild3 (FK: orgChild2Id, orgChild1Id, orgRootId, orgRevisionId) + │ │ │ │ └── orgChild4 (FK: orgChild3Id, orgChild2Id, orgChild1Id, orgRootId, orgRevisionId) + │ │ │ │ + │ │ │ └── posMaster (FK: orgRootId, orgChild1Id, orgChild2Id, orgChild3Id, orgChild4Id) + │ │ │ └── position (FK: posMasterId) + │ │ │ + │ │ └── posMaster + │ │ + │ └── posMaster + │ + └── (current revision similar structure) +``` + +--- + +## Error Handling + +| Error Condition | Response | +|----------------|----------| +| Draft/Current revision not found | 404 NOT_FOUND | +| rootDnaId not found in draft | 404 NOT_FOUND | +| No positions in draft structure | 404 NOT_FOUND | +| Database error during sync | 500 INTERNAL_SERVER_ERROR (rollback) | + +--- + +## Files Modified/Created + +``` +src/ +├── interfaces/ +│ └── OrgMapping.ts [NEW] Type definitions +├── controllers/ +│ └── OrganizationController.ts [MODIFIED] moveDraftToCurrent function + helpers +docs/ +└── move-draft-to-current.md [NEW] This documentation +``` + +--- + +## Testing Considerations + +### Unit Tests + +- Test `syncOrgLevel()` for each org level with various scenarios +- Test `resolveOrgId()` mapping function +- Test `cascadeDeletePositions()` function +- Test `syncPositionsForPosMaster()` function + +### Integration Tests + +1. **Empty Draft** - Sync with no draft data +2. **Full Replacement** - All nodes changed +3. **Partial Update** - Some nodes added, some updated, some deleted +4. **Position Sync** - Verify position table syncs correctly +5. **Foreign Key Constraints** - Verify all FK relationships maintained + +### Manual Testing Flow + +1. Create draft structure with various changes: + - Add new department + - Modify existing department + - Remove existing department + - Add/modify/remove positions +2. Call `move-draft-to-current` API +3. Verify: + - New departments appear in current + - Modified departments are updated (not recreated) + - Removed departments are gone (with positions cascade deleted) + - Positions have correct new org IDs + - Position table records sync correctly + - All foreign key constraints satisfied + +--- + +## Performance Optimization + +- **Batch Operations** - Use `In()` clause for multiple IDs +- **Map Lookups** - Use `Map` for O(1) lookups instead of array searches +- **Bottom-Up Processing** - Minimize FK constraint checks +- **Parallel Queries** - Use `Promise.all()` for independent queries + +--- + +## Future Improvements + +1. **Parallel Processing** - Process independent org branches in parallel +2. **Incremental Sync** - Only sync changed subtrees +3. **Caching** - Cache org mappings for repeated operations +4. **Audit Log** - Track all changes for audit purposes +5. **Validation** - Add pre-sync validation to catch errors early + +--- + +## References + +- TypeORM Documentation: https://typeorm.io/ +- TSOA Documentation: https://tsoa-community.github.io/ +- Project Repository: [Internal Git] + +--- + +**Last Updated:** 2025-02-09 +**Author:** Claude Code +**Version:** 1.0.0 diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 9cd6dd6a..1a919569 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -24,7 +24,7 @@ import HttpSuccess from "../interfaces/http-success"; import { OrgChild1 } from "../entities/OrgChild1"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; -import { In, IsNull, Not } from "typeorm"; +import { In, IsNull, Not, Like } from "typeorm"; import { OrgRoot } from "../entities/OrgRoot"; import { OrgChild2 } from "../entities/OrgChild2"; import { OrgChild3 } from "../entities/OrgChild3"; @@ -57,6 +57,7 @@ import { CreatePosMasterHistoryOfficer, } from "../services/PositionService"; import { orgStructureCache } from "../utils/OrgStructureCache"; +import { OrgIdMapping, AllOrgMappings } from "../interfaces/OrgMapping"; @Route("api/v1/org") @Tags("Organization") @@ -7811,34 +7812,567 @@ export class OrganizationController extends Controller { * @summary - ย้ายโครงสร้างและตำแหน่งจากแบบร่างไปโครงสร้างปัจจุบัน โดยอ้างอิงตาม rootId * */ - @Post("move-draft-to-current/{rootId}") + @Post("move-draft-to-current/{rootDnaId}") async moveDraftToCurrent(@Request() request: RequestWithUser) { - // part 1 ข้อมูลโครงสร้าง - const drafRevision = await this.orgRevisionRepository.findOne({ - where: { - orgRevisionIsDraft: true, - orgRevisionIsCurrent: false, - }, - select: ["id"], - }); + const queryRunner = AppDataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - const currentRevision = await this.orgRevisionRepository.findOne({ - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - select: ["id"], - }); + try { + // permission owner only ?? + // this code check... - if (!drafRevision || !currentRevision) - return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล"); + // part 1 ข้อมูลโครงสร้าง + const [drafRevision, currentRevision] = await Promise.all([ + this.orgRevisionRepository.findOne({ + where: { + orgRevisionIsDraft: true, + orgRevisionIsCurrent: false, + }, + select: ["id"], + }), + this.orgRevisionRepository.findOne({ + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + select: ["id"], + }), + ]); - const drafRevisionId = drafRevision.id; - const currentRevisionId = currentRevision.id; + if (!drafRevision || !currentRevision) + return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล"); - // start transaction multi table orgRoot, orgChild1, orgChild2, orgChild3, orgChild4, posMaster, position + const drafRevisionId = drafRevision.id; + const currentRevisionId = currentRevision.id; - // part 2 ข้อมูลตำแหน่ง - // 1. ดึงข้อมูลคนออกจากโครงสร้างปัจจุบัน + // ตรวจสอบว่ามี rootDnaId ในโครงสร้างร่าง และในโครงสร้างปัจจุบันหรือไม่ + const [orgRootDraft, orgRootCurrent] = await Promise.all([ + this.orgRootRepository.findOne({ + where: { + ancestorDNA: request.params.rootDnaId, + orgRevisionId: drafRevisionId, + }, + select: ["id"], + }), + this.orgRootRepository.findOne({ + where: { + ancestorDNA: request.params.rootDnaId, + orgRevisionId: currentRevisionId, + }, + select: ["id"], + }), + ]); + + if (!orgRootDraft) return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้างร่าง"); + + // Part 1: Differential sync of organization structure (bottom-up) + // Build mapping incrementally as we process each level + const allMappings: AllOrgMappings = { + orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() } + }; + + // Process from bottom (Child4) to top (Root) to handle foreign key constraints + // Child4 (leaf nodes - no children depending on them) + allMappings.orgChild4 = await this.syncOrgLevel( + queryRunner, OrgChild4, this.child4Repository, + drafRevisionId, currentRevisionId, + request.params.rootDnaId, allMappings + ); + + // Child3 + allMappings.orgChild3 = await this.syncOrgLevel( + queryRunner, OrgChild3, this.child3Repository, + drafRevisionId, currentRevisionId, + request.params.rootDnaId, allMappings + ); + + // Child2 + allMappings.orgChild2 = await this.syncOrgLevel( + queryRunner, OrgChild2, this.child2Repository, + drafRevisionId, currentRevisionId, + request.params.rootDnaId, allMappings + ); + + // Child1 + allMappings.orgChild1 = await this.syncOrgLevel( + queryRunner, OrgChild1, this.child1Repository, + drafRevisionId, currentRevisionId, + request.params.rootDnaId, allMappings + ); + + // OrgRoot (root level - no parent mapping needed) + allMappings.orgRoot = await this.syncOrgLevel( + queryRunner, OrgRoot, this.orgRootRepository, + drafRevisionId, currentRevisionId, + request.params.rootDnaId + ); + + // Part 2: Sync position data using new org IDs from Part 1 + // 2.1 Clear current_holderId for affected positions (keep existing logic) + // Get draft organization IDs under the given rootDnaId to find positions to clear + const draftOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.keys()], + orgChild1: [...allMappings.orgChild1.byDraftId.keys()], + orgChild2: [...allMappings.orgChild2.byDraftId.keys()], + orgChild3: [...allMappings.orgChild3.byDraftId.keys()], + orgChild4: [...allMappings.orgChild4.byDraftId.keys()], + }; + + // Get draft positions that belong to any org under the rootDnaId + const posMasterDraft = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) }, + { orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) }, + { orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) }, + { orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) }, + { orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) }, + ], + }); + + if (posMasterDraft.length <= 0) + return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); + + // Clear current_holderId for positions that will have new holders + const nextHolderIds = posMasterDraft + .filter(x => x.next_holderId != null) + .map(x => x.next_holderId); + + if (nextHolderIds.length > 0) { + await queryRunner.manager.update( + PosMaster, + { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds) + }, + { current_holderId: null, isSit: false } + ); + } + + // 2.2 Fetch current positions for comparison + // Get current organization IDs from the mappings + const currentOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.values()], + orgChild1: [...allMappings.orgChild1.byDraftId.values()], + orgChild2: [...allMappings.orgChild2.byDraftId.values()], + orgChild3: [...allMappings.orgChild3.byDraftId.values()], + orgChild4: [...allMappings.orgChild4.byDraftId.values()], + }; + + const posMasterCurrent = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, + { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, + { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, + { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, + { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, + ], + }); + + // Build lookup map + const currentByDNA = new Map( + posMasterCurrent.map(p => [p.ancestorDNA, p]) + ); + + // 2.3 Batch DELETE: positions in current but not in draft + const toDelete = posMasterCurrent.filter( + curr => !posMasterDraft.some(d => d.ancestorDNA === curr.ancestorDNA) + ); + + if (toDelete.length > 0) { + const toDeleteIds = toDelete.map(p => p.id); + + // Cascade delete positions first + await queryRunner.manager.delete( + Position, + { posMasterId: In(toDeleteIds) } + ); + + // Then delete posMaster records + await queryRunner.manager.delete( + PosMaster, + toDeleteIds + ); + } + + // 2.4 Process draft positions (UPDATE or INSERT) + const toUpdate: PosMaster[] = []; + const toInsert: any[] = []; + + // Track draft PosMaster ID to current PosMaster ID mapping for position sync + const posMasterMapping: Map = new Map(); + + for (const draftPos of posMasterDraft) { + const current = currentByDNA.get(draftPos.ancestorDNA); + + // Map organization IDs using new IDs from Part 1 + const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot); + const orgChild1Id = this.resolveOrgId(draftPos.orgChild1Id ?? null, allMappings.orgChild1); + const orgChild2Id = this.resolveOrgId(draftPos.orgChild2Id ?? null, allMappings.orgChild2); + const orgChild3Id = this.resolveOrgId(draftPos.orgChild3Id ?? null, allMappings.orgChild3); + const orgChild4Id = this.resolveOrgId(draftPos.orgChild4Id ?? null, allMappings.orgChild4); + + if (current) { + // UPDATE existing position + Object.assign(current, { + createdAt: draftPos.createdAt, + createdUserId: draftPos.createdUserId, + createdFullName: draftPos.createdFullName, + lastUpdatedAt: new Date(), + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + posMasterNoPrefix: draftPos.posMasterNoPrefix, + posMasterNoSuffix: draftPos.posMasterNoSuffix, + posMasterNo: draftPos.posMasterNo, + posMasterOrder: draftPos.posMasterOrder, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + isSit: draftPos.isSit, + reason: draftPos.reason, + isDirector: draftPos.isDirector, + isStaff: draftPos.isStaff, + positionSign: draftPos.positionSign, + statusReport: "DONE", + isCondition: draftPos.isCondition, + conditionReason: draftPos.conditionReason, + }); + toUpdate.push(current); + + // Track mapping for position sync + posMasterMapping.set(draftPos.id, current.id); + } else { + // INSERT new position + const newPosMaster = queryRunner.manager.create(PosMaster, { + ...draftPos, + id: undefined, + orgRevisionId: currentRevisionId, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + statusReport: "DONE", + }); + toInsert.push(newPosMaster); + } + } + + // Batch save updates and inserts + if (toUpdate.length > 0) { + await queryRunner.manager.save(toUpdate); + } + if (toInsert.length > 0) { + const saved = await queryRunner.manager.save(toInsert); + + // Track mapping for newly inserted posMasters + // saved is an array, map each to its draft ID + if (Array.isArray(saved)) { + for (let i = 0; i < saved.length; i++) { + const draftPos = posMasterDraft.filter(d => !currentByDNA.has(d.ancestorDNA))[i]; + if (draftPos && saved[i]) { + posMasterMapping.set(draftPos.id, saved[i].id); + } + } + } + } + + // 2.5 Sync positions table for all affected posMasters + for (const [draftPosMasterId, currentPosMasterId] of posMasterMapping) { + await this.syncPositionsForPosMaster( + queryRunner, + draftPosMasterId, + currentPosMasterId, + drafRevisionId, + currentRevisionId + ); + } + + await queryRunner.commitTransaction(); + } catch (error) { + console.error("Error moving draft to current:", error); + await queryRunner.rollbackTransaction(); + throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการย้ายโครงสร้าง"); + } + } + + /** + * Helper function: Map draft ID to current ID using the mapping + */ + private resolveOrgId( + draftId: string | null, + mapping: OrgIdMapping + ): string | null { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + } + + /** + * Helper function: Cascade delete positions before deleting org node + */ + private async cascadeDeletePositions( + queryRunner: any, + node: any, + entityClass: any + ): Promise { + const whereClause: any = { + orgRevisionId: node.orgRevisionId + }; + + // Determine which FK field to use based on entity type + if (entityClass === OrgRoot) { + whereClause.orgRootId = node.id; + } else if (entityClass === OrgChild1) { + whereClause.orgChild1Id = node.id; + } else if (entityClass === OrgChild2) { + whereClause.orgChild2Id = node.id; + } else if (entityClass === OrgChild3) { + whereClause.orgChild3Id = node.id; + } else if (entityClass === OrgChild4) { + whereClause.orgChild4Id = node.id; + } + + await queryRunner.manager.delete(PosMaster, whereClause); + } + + /** + * Helper function: Generic differential sync for each org level + * Performs DELETE (nodes not in draft), UPDATE (existing nodes), INSERT (new nodes) + */ + private async syncOrgLevel( + queryRunner: any, + entityClass: any, + repository: any, + draftRevisionId: string, + currentRevisionId: string, + rootDnaId: string, + parentMappings?: AllOrgMappings + ): Promise { + // 1. Fetch draft and current nodes under the given rootDnaId + const [draftNodes, currentNodes] = await Promise.all([ + repository.find({ + where: { + orgRevisionId: draftRevisionId, + ancestorDNA: Like(`${rootDnaId}%`) + } + }), + repository.find({ + where: { + orgRevisionId: currentRevisionId, + ancestorDNA: Like(`${rootDnaId}%`) + } + }) + ]); + + // 2. Build lookup maps for efficient matching by ancestorDNA + const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n])); + const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); + + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map() + }; + + // 3. DELETE: Current nodes not in draft (cascade delete positions first) + const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA)); + for (const node of toDelete) { + // Cascade delete positions first + await this.cascadeDeletePositions(queryRunner, node, entityClass); + await queryRunner.manager.delete(entityClass, node.id); + } + + // 4. UPDATE: Nodes that exist in both draft and current (matched by ancestorDNA) + const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA)); + for (const draft of toUpdate) { + const current: any = currentByDNA.get(draft.ancestorDNA)!; + + // Build update data with mapped parent IDs + const updateData: any = { + ...draft, + id: current.id, + orgRevisionId: currentRevisionId, + }; + + // Map parent IDs based on entity level + if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) { + updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } else if (entityClass === OrgChild2) { + if (draft.orgRootId && parentMappings) { + updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } + if (draft.orgChild1Id && parentMappings) { + updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + } + } else if (entityClass === OrgChild3) { + if (draft.orgRootId && parentMappings) { + updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } + if (draft.orgChild1Id && parentMappings) { + updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + } + if (draft.orgChild2Id && parentMappings) { + updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + } + } else if (entityClass === OrgChild4) { + if (draft.orgRootId && parentMappings) { + updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } + if (draft.orgChild1Id && parentMappings) { + updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + } + if (draft.orgChild2Id && parentMappings) { + updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + } + if (draft.orgChild3Id && parentMappings) { + updateData.orgChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id) ?? draft.orgChild3Id; + } + } + + await queryRunner.manager.update(entityClass, current.id, updateData); + + mapping.byAncestorDNA.set(draft.ancestorDNA, current.id); + mapping.byDraftId.set(draft.id, current.id); + } + + // 5. INSERT: Draft nodes not in current + const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA)); + for (const draft of toInsert) { + const newNode: any = queryRunner.manager.create(entityClass, { + ...draft, + id: undefined, + orgRevisionId: currentRevisionId, + }); + + // Map parent IDs based on entity level + if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } else if (entityClass === OrgChild2) { + if (draft.orgRootId && parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + if (draft.orgChild1Id && parentMappings) { + newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + } + } else if (entityClass === OrgChild3) { + if (draft.orgRootId && parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + if (draft.orgChild1Id && parentMappings) { + newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + } + if (draft.orgChild2Id && parentMappings) { + newNode.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + } + } else if (entityClass === OrgChild4) { + if (draft.orgRootId && parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + if (draft.orgChild1Id && parentMappings) { + newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + } + if (draft.orgChild2Id && parentMappings) { + newNode.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + } + if (draft.orgChild3Id && parentMappings) { + newNode.orgChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id); + } + } + + const saved = await queryRunner.manager.save(newNode); + + mapping.byAncestorDNA.set(draft.ancestorDNA, saved.id); + mapping.byDraftId.set(draft.id, saved.id); + } + + return mapping; + } + + /** + * Helper function: Sync positions for a PosMaster + * Handles DELETE/UPDATE/INSERT for positions associated with a posMaster + */ + private async syncPositionsForPosMaster( + queryRunner: any, + draftPosMasterId: string, + currentPosMasterId: string, + draftRevisionId: string, + currentRevisionId: string + ): Promise { + // Fetch draft and current positions for this posMaster + const [draftPositions, currentPositions] = await Promise.all([ + queryRunner.manager.find(Position, { + where: { + posMasterId: draftPosMasterId + }, + order: { orderNo: 'ASC' } + }), + queryRunner.manager.find(Position, { + where: { + posMasterId: currentPosMasterId + } + }) + ]); + + // If no draft positions, delete all current positions + if (draftPositions.length === 0) { + if (currentPositions.length > 0) { + await queryRunner.manager.delete( + Position, + currentPositions.map((p: any) => p.id) + ); + } + return; + } + + // Build maps for tracking + const currentByOrderNo = new Map( + currentPositions.map((p: any) => [p.orderNo, p]) + ); + + // DELETE: Current positions not in draft (by orderNo) + const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); + const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo)); + + if (toDelete.length > 0) { + await queryRunner.manager.delete( + Position, + toDelete.map((p: any) => p.id) + ); + } + + // UPDATE and INSERT + for (const draftPos of draftPositions) { + const current: any = currentByOrderNo.get(draftPos.orderNo); + + if (current) { + // UPDATE existing position + await queryRunner.manager.update(Position, current.id, { + positionName: draftPos.positionName, + positionField: draftPos.positionField, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + posExecutiveId: draftPos.posExecutiveId, + positionExecutiveField: draftPos.positionExecutiveField, + positionArea: draftPos.positionArea, + isSpecial: draftPos.isSpecial, + }); + } else { + // INSERT new position + const newPosition = queryRunner.manager.create(Position, { + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }); + await queryRunner.manager.save(newPosition); + } + } } } diff --git a/src/interfaces/OrgMapping.ts b/src/interfaces/OrgMapping.ts new file mode 100644 index 00000000..1cdea98f --- /dev/null +++ b/src/interfaces/OrgMapping.ts @@ -0,0 +1,24 @@ +/** + * Type definitions for organization mapping used in move-draft-to-current function + */ + +/** + * Maps draft organization IDs to current organization IDs + * - byAncestorDNA: Maps ancestorDNA to current ID for lookup + * - byDraftId: Maps draft ID to current ID for position updates + */ +export interface OrgIdMapping { + byAncestorDNA: Map; + byDraftId: Map; +} + +/** + * Contains mappings for all organization levels + */ +export interface AllOrgMappings { + orgRoot: OrgIdMapping; + orgChild1: OrgIdMapping; + orgChild2: OrgIdMapping; + orgChild3: OrgIdMapping; + orgChild4: OrgIdMapping; +} From f9d626a499f77af3c4754f988656d07da4ee2b06 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 9 Feb 2026 17:45:50 +0700 Subject: [PATCH 058/178] add test, and fix script --- jest.config.js | 27 + package.json | 8 +- src/__tests__/setup.ts | 17 + src/__tests__/unit/OrgMapping.spec.ts | 54 ++ .../unit/OrganizationController.spec.ts | 460 ++++++++++++++++++ src/controllers/OrganizationController.ts | 160 +++--- 6 files changed, 653 insertions(+), 73 deletions(-) create mode 100644 jest.config.js create mode 100644 src/__tests__/setup.ts create mode 100644 src/__tests__/unit/OrgMapping.spec.ts create mode 100644 src/__tests__/unit/OrganizationController.spec.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..7534d46d --- /dev/null +++ b/jest.config.js @@ -0,0 +1,27 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.spec.ts', '**/__tests__/**/*.test.ts'], + transform: { + '^.+\\.ts$': ['ts-jest', { + diagnostics: { + ignoreCodes: [151002], + }, + }], + }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/app.ts', + '!src/database/**', + '!src/__tests__/**', + ], + coverageDirectory: 'coverage', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + setupFilesAfterEnv: ['/src/__tests__/setup.ts'], + testTimeout: 10000, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; diff --git a/package.json b/package.json index 7fb9c551..ac8d5ca7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "format": "prettier --write .", "build": "tsoa spec-and-routes && tsc", "migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/database/data-source.ts", - "migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts" + "migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "keywords": [], "author": "", @@ -19,12 +22,15 @@ "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", "@types/node": "^20.11.5", "@types/node-cron": "^3.0.11", "@types/swagger-ui-express": "^4.1.6", "@types/ws": "^8.5.14", + "jest": "^29.7.0", "nodemon": "^3.0.3", "prettier": "^3.2.2", + "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 00000000..04ef89ef --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,17 @@ +// Test setup file for Jest +// Mock environment variables +process.env.NODE_ENV = 'test'; +process.env.DB_HOST = 'localhost'; +process.env.DB_PORT = '3306'; +process.env.DB_USERNAME = 'test'; +process.env.DB_PASSWORD = 'test'; +process.env.DB_DATABASE = 'test_db'; + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + log: jest.fn(), +}; diff --git a/src/__tests__/unit/OrgMapping.spec.ts b/src/__tests__/unit/OrgMapping.spec.ts new file mode 100644 index 00000000..c59e5697 --- /dev/null +++ b/src/__tests__/unit/OrgMapping.spec.ts @@ -0,0 +1,54 @@ +/** + * Unit tests for move-draft-to-current helper functions + */ + +import { OrgIdMapping, AllOrgMappings } from '../../interfaces/OrgMapping'; + +// Mock dependencies +jest.mock('../../database/data-source', () => ({ + AppDataSource: { + createQueryRunner: jest.fn(), + }, +})); + +describe('OrgMapping Interfaces', () => { + describe('OrgIdMapping', () => { + it('should create a valid OrgIdMapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + expect(mapping.byAncestorDNA).toBeInstanceOf(Map); + expect(mapping.byDraftId).toBeInstanceOf(Map); + }); + + it('should store and retrieve values correctly', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map([['dna1', 'id1']]), + byDraftId: new Map([['draftId1', 'currentId1']]), + }; + + expect(mapping.byAncestorDNA.get('dna1')).toBe('id1'); + expect(mapping.byDraftId.get('draftId1')).toBe('currentId1'); + }); + }); + + describe('AllOrgMappings', () => { + it('should create a valid AllOrgMappings', () => { + const mappings: AllOrgMappings = { + orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, + }; + + expect(mappings.orgRoot).toBeDefined(); + expect(mappings.orgChild1).toBeDefined(); + expect(mappings.orgChild2).toBeDefined(); + expect(mappings.orgChild3).toBeDefined(); + expect(mappings.orgChild4).toBeDefined(); + }); + }); +}); diff --git a/src/__tests__/unit/OrganizationController.spec.ts b/src/__tests__/unit/OrganizationController.spec.ts new file mode 100644 index 00000000..615298bc --- /dev/null +++ b/src/__tests__/unit/OrganizationController.spec.ts @@ -0,0 +1,460 @@ +/** + * Unit tests for OrganizationController move-draft-to-current helper functions + */ + +import { OrgIdMapping } from '../../interfaces/OrgMapping'; + +// Mock typeorm +jest.mock('typeorm', () => ({ + Entity: jest.fn(), + Column: jest.fn(), + ManyToOne: jest.fn(), + JoinColumn: jest.fn(), + OneToMany: jest.fn(), + In: jest.fn((val: any) => val), + Like: jest.fn((val: any) => val), + IsNull: jest.fn(), + Not: jest.fn(), +})); + +// Mock entities +jest.mock('../../entities/OrgRoot', () => ({ OrgRoot: {} })); +jest.mock('../../entities/OrgChild1', () => ({ OrgChild1: {} })); +jest.mock('../../entities/OrgChild2', () => ({ OrgChild2: {} })); +jest.mock('../../entities/OrgChild3', () => ({ OrgChild3: {} })); +jest.mock('../../entities/OrgChild4', () => ({ OrgChild4: {} })); +jest.mock('../../entities/PosMaster', () => ({ PosMaster: {} })); +jest.mock('../../entities/Position', () => ({ Position: {} })); + +// Import after mocking +import { In, Like } from 'typeorm'; +import { OrgRoot } from '../../entities/OrgRoot'; +import { OrgChild1 } from '../../entities/OrgChild1'; +import { OrgChild2 } from '../../entities/OrgChild2'; +import { OrgChild3 } from '../../entities/OrgChild3'; +import { OrgChild4 } from '../../entities/OrgChild4'; +import { PosMaster } from '../../entities/PosMaster'; +import { Position } from '../../entities/Position'; + +describe('OrganizationController - Helper Functions', () => { + let mockQueryRunner: any; + let mockController: any; + + beforeEach(() => { + // Mock queryRunner + mockQueryRunner = { + manager: { + find: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }; + + // Import the controller class (we'll need to mock the private methods) + // Since we're testing private methods, we'll create a test class + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('resolveOrgId()', () => { + it('should return null when draftId is null', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + // Simulate the function logic + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId(null, mapping)).toBeNull(); + }); + + it('should return null when draftId is undefined', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + const resolveOrgId = (draftId: string | null | undefined, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId(undefined, mapping)).toBeNull(); + }); + + it('should return mapped ID when draftId exists in mapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map([['draft1', 'current1']]), + }; + + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId('draft1', mapping)).toBe('current1'); + }); + + it('should return null when draftId does not exist in mapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId('nonexistent', mapping)).toBeNull(); + }); + }); + + describe('cascadeDeletePositions()', () => { + it('should delete positions with orgRootId when entityClass is OrgRoot', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgRootId: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgRootId: 'node1', + }); + }); + + it('should delete positions with orgChild1Id when entityClass is OrgChild1', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild1Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild1Id: 'node1', + }); + }); + + it('should delete positions with orgChild2Id when entityClass is OrgChild2', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild2Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild2Id: 'node1', + }); + }); + + it('should delete positions with orgChild3Id when entityClass is OrgChild3', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild3Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild3Id: 'node1', + }); + }); + + it('should delete positions with orgChild4Id when entityClass is OrgChild4', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild4Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild4Id: 'node1', + }); + }); + }); + + describe('syncOrgLevel()', () => { + beforeEach(() => { + mockQueryRunner.manager.find.mockResolvedValue([]); + mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.create.mockReturnValue({}); + mockQueryRunner.manager.save.mockResolvedValue({}); + }); + + it('should fetch draft and current nodes with Like filter', async () => { + const repository = { + find: jest.fn().mockResolvedValue([]), + }; + + await repository.find({ + where: { + orgRevisionId: 'draftRev1', + ancestorDNA: Like('root-dna%'), + }, + }); + + await repository.find({ + where: { + orgRevisionId: 'currentRev1', + ancestorDNA: Like('root-dna%'), + }, + }); + + expect(repository.find).toHaveBeenCalledTimes(2); + }); + + it('should build lookup maps from draft and current nodes', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const draftByDNA = new Map(draftNodes.map(n => [n.ancestorDNA, n])); + const currentByDNA = new Map(currentNodes.map(n => [n.ancestorDNA, n])); + + expect(draftByDNA.size).toBe(2); + expect(currentByDNA.size).toBe(1); + expect(draftByDNA.get('root-dna/child1')).toEqual(draftNodes[0]); + expect(currentByDNA.get('root-dna/child1')).toEqual(currentNodes[0]); + }); + + it('should identify nodes to delete (in current but not in draft)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + { id: 'current2', ancestorDNA: 'root-dna/child2' }, // Not in draft + ]; + + const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n])); + const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA)); + + expect(toDelete).toHaveLength(1); + expect(toDelete[0].id).toBe('current2'); + }); + + it('should identify nodes to update (in both draft and current)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); + const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA)); + + expect(toUpdate).toHaveLength(1); + expect(toUpdate[0].id).toBe('draft1'); + }); + + it('should identify nodes to insert (in draft but not in current)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); + const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA)); + + expect(toInsert).toHaveLength(1); + expect(toInsert[0].id).toBe('draft2'); + }); + + it('should return correct mapping after sync', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map([ + ['root-dna/child1', 'current1'], + ['root-dna/child2', 'current2'], + ]), + byDraftId: new Map([ + ['draft1', 'current1'], + ['draft2', 'current2'], + ]), + }; + + expect(mapping.byAncestorDNA.get('root-dna/child1')).toBe('current1'); + expect(mapping.byDraftId.get('draft1')).toBe('current1'); + expect(mapping.byDraftId.get('draft2')).toBe('current2'); + }); + }); + + describe('syncPositionsForPosMaster()', () => { + beforeEach(() => { + mockQueryRunner.manager.find.mockResolvedValue([]); + mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.create.mockReturnValue({}); + mockQueryRunner.manager.save.mockResolvedValue({}); + }); + + it('should fetch draft and current positions for a posMaster', async () => { + const draftPosMasterId = 'draft-pos-1'; + const currentPosMasterId = 'current-pos-1'; + + mockQueryRunner.manager.find + .mockResolvedValueOnce([{ id: 'pos1', posMasterId: draftPosMasterId }]) + .mockResolvedValueOnce([{ id: 'pos2', posMasterId: currentPosMasterId }]); + + await mockQueryRunner.manager.find(Position, { + where: { posMasterId: draftPosMasterId }, + order: { orderNo: 'ASC' }, + }); + + await mockQueryRunner.manager.find(Position, { + where: { posMasterId: currentPosMasterId }, + }); + + expect(mockQueryRunner.manager.find).toHaveBeenCalledTimes(2); + }); + + it('should delete all current positions when no draft positions exist', async () => { + const currentPositions = [ + { id: 'pos1', orderNo: 1 }, + { id: 'pos2', orderNo: 2 }, + ]; + + mockQueryRunner.manager.find + .mockResolvedValueOnce([]) // No draft positions + .mockResolvedValueOnce(currentPositions); + + await mockQueryRunner.manager.delete(Position, ['pos1', 'pos2']); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(Position, ['pos1', 'pos2']); + }); + + it('should delete positions not in draft (by orderNo)', async () => { + const draftPositions = [ + { id: 'dpos1', orderNo: 1 }, + { id: 'dpos2', orderNo: 2 }, + ]; + const currentPositions = [ + { id: 'cpos1', orderNo: 1 }, + { id: 'cpos2', orderNo: 2 }, + { id: 'cpos3', orderNo: 3 }, // Not in draft + ]; + + mockQueryRunner.manager.find + .mockResolvedValueOnce(draftPositions) + .mockResolvedValueOnce(currentPositions); + + const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); + const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo)); + + expect(toDelete).toHaveLength(1); + expect(toDelete[0].id).toBe('cpos3'); + }); + + it('should update existing positions (matched by orderNo)', async () => { + const draftPositions = [ + { + id: 'dpos1', + orderNo: 1, + positionName: 'Updated Name', + positionField: 'field1', + posTypeId: 'type1', + posLevelId: 'level1', + }, + ]; + const currentPositions = [ + { id: 'cpos1', orderNo: 1, positionName: 'Old Name' }, + ]; + + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftPos = draftPositions[0]; + const current = currentByOrderNo.get(draftPos.orderNo); + + expect(current).toBeDefined(); + + if (current) { + const updateData = { + positionName: draftPos.positionName, + positionField: draftPos.positionField, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + }; + + await mockQueryRunner.manager.update(Position, current.id, updateData); + + expect(mockQueryRunner.manager.update).toHaveBeenCalledWith( + Position, + 'cpos1', + expect.objectContaining({ positionName: 'Updated Name' }) + ); + } + }); + + it('should insert new positions not in current', async () => { + const draftPositions = [ + { id: 'dpos1', orderNo: 1, positionName: 'New Position' }, + ]; + const currentPositions: any[] = []; + + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftPos = draftPositions[0]; + const current = currentByOrderNo.get(draftPos.orderNo); + const currentPosMasterId = 'current-pos-1'; + + expect(current).toBeUndefined(); + + if (!current) { + const newPosition = { + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }; + + await mockQueryRunner.manager.create(Position, newPosition); + await mockQueryRunner.manager.save(newPosition); + + expect(mockQueryRunner.manager.create).toHaveBeenCalledWith(Position, { + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }); + } + }); + }); +}); diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 1a919569..02e35abf 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7813,7 +7813,7 @@ export class OrganizationController extends Controller { * */ @Post("move-draft-to-current/{rootDnaId}") - async moveDraftToCurrent(@Request() request: RequestWithUser) { + async moveDraftToCurrent(@Path() rootDnaId: string, @Request() request: RequestWithUser) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -7850,14 +7850,14 @@ export class OrganizationController extends Controller { const [orgRootDraft, orgRootCurrent] = await Promise.all([ this.orgRootRepository.findOne({ where: { - ancestorDNA: request.params.rootDnaId, + ancestorDNA: rootDnaId, orgRevisionId: drafRevisionId, }, select: ["id"], }), this.orgRootRepository.findOne({ where: { - ancestorDNA: request.params.rootDnaId, + ancestorDNA: rootDnaId, orgRevisionId: currentRevisionId, }, select: ["id"], @@ -7873,43 +7873,62 @@ export class OrganizationController extends Controller { orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() } + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, }; // Process from bottom (Child4) to top (Root) to handle foreign key constraints // Child4 (leaf nodes - no children depending on them) allMappings.orgChild4 = await this.syncOrgLevel( - queryRunner, OrgChild4, this.child4Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild4, + this.child4Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // Child3 allMappings.orgChild3 = await this.syncOrgLevel( - queryRunner, OrgChild3, this.child3Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild3, + this.child3Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // Child2 allMappings.orgChild2 = await this.syncOrgLevel( - queryRunner, OrgChild2, this.child2Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild2, + this.child2Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // Child1 allMappings.orgChild1 = await this.syncOrgLevel( - queryRunner, OrgChild1, this.child1Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild1, + this.child1Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // OrgRoot (root level - no parent mapping needed) allMappings.orgRoot = await this.syncOrgLevel( - queryRunner, OrgRoot, this.orgRootRepository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId + queryRunner, + OrgRoot, + this.orgRootRepository, + drafRevisionId, + currentRevisionId, + rootDnaId, ); // Part 2: Sync position data using new org IDs from Part 1 @@ -7939,17 +7958,17 @@ export class OrganizationController extends Controller { // Clear current_holderId for positions that will have new holders const nextHolderIds = posMasterDraft - .filter(x => x.next_holderId != null) - .map(x => x.next_holderId); + .filter((x) => x.next_holderId != null) + .map((x) => x.next_holderId); if (nextHolderIds.length > 0) { await queryRunner.manager.update( PosMaster, { orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds) + current_holderId: In(nextHolderIds), }, - { current_holderId: null, isSit: false } + { current_holderId: null, isSit: false }, ); } @@ -7974,29 +7993,21 @@ export class OrganizationController extends Controller { }); // Build lookup map - const currentByDNA = new Map( - posMasterCurrent.map(p => [p.ancestorDNA, p]) - ); + const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p])); // 2.3 Batch DELETE: positions in current but not in draft const toDelete = posMasterCurrent.filter( - curr => !posMasterDraft.some(d => d.ancestorDNA === curr.ancestorDNA) + (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), ); if (toDelete.length > 0) { - const toDeleteIds = toDelete.map(p => p.id); + const toDeleteIds = toDelete.map((p) => p.id); // Cascade delete positions first - await queryRunner.manager.delete( - Position, - { posMasterId: In(toDeleteIds) } - ); + await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); // Then delete posMaster records - await queryRunner.manager.delete( - PosMaster, - toDeleteIds - ); + await queryRunner.manager.delete(PosMaster, toDeleteIds); } // 2.4 Process draft positions (UPDATE or INSERT) @@ -8077,7 +8088,7 @@ export class OrganizationController extends Controller { // saved is an array, map each to its draft ID if (Array.isArray(saved)) { for (let i = 0; i < saved.length; i++) { - const draftPos = posMasterDraft.filter(d => !currentByDNA.has(d.ancestorDNA))[i]; + const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; if (draftPos && saved[i]) { posMasterMapping.set(draftPos.id, saved[i].id); } @@ -8092,7 +8103,7 @@ export class OrganizationController extends Controller { draftPosMasterId, currentPosMasterId, drafRevisionId, - currentRevisionId + currentRevisionId, ); } @@ -8107,10 +8118,7 @@ export class OrganizationController extends Controller { /** * Helper function: Map draft ID to current ID using the mapping */ - private resolveOrgId( - draftId: string | null, - mapping: OrgIdMapping - ): string | null { + private resolveOrgId(draftId: string | null, mapping: OrgIdMapping): string | null { if (!draftId) return null; return mapping.byDraftId.get(draftId) ?? null; } @@ -8121,10 +8129,10 @@ export class OrganizationController extends Controller { private async cascadeDeletePositions( queryRunner: any, node: any, - entityClass: any + entityClass: any, ): Promise { const whereClause: any = { - orgRevisionId: node.orgRevisionId + orgRevisionId: node.orgRevisionId, }; // Determine which FK field to use based on entity type @@ -8154,22 +8162,22 @@ export class OrganizationController extends Controller { draftRevisionId: string, currentRevisionId: string, rootDnaId: string, - parentMappings?: AllOrgMappings + parentMappings?: AllOrgMappings, ): Promise { // 1. Fetch draft and current nodes under the given rootDnaId const [draftNodes, currentNodes] = await Promise.all([ repository.find({ where: { orgRevisionId: draftRevisionId, - ancestorDNA: Like(`${rootDnaId}%`) - } + ancestorDNA: Like(`${rootDnaId}%`), + }, }), repository.find({ where: { orgRevisionId: currentRevisionId, - ancestorDNA: Like(`${rootDnaId}%`) - } - }) + ancestorDNA: Like(`${rootDnaId}%`), + }, + }), ]); // 2. Build lookup maps for efficient matching by ancestorDNA @@ -8178,7 +8186,7 @@ export class OrganizationController extends Controller { const mapping: OrgIdMapping = { byAncestorDNA: new Map(), - byDraftId: new Map() + byDraftId: new Map(), }; // 3. DELETE: Current nodes not in draft (cascade delete positions first) @@ -8203,36 +8211,46 @@ export class OrganizationController extends Controller { // Map parent IDs based on entity level if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } else if (entityClass === OrgChild2) { if (draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } if (draft.orgChild1Id && parentMappings) { - updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; } } else if (entityClass === OrgChild3) { if (draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } if (draft.orgChild1Id && parentMappings) { - updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; } if (draft.orgChild2Id && parentMappings) { - updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + updateData.orgChild2Id = + parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; } } else if (entityClass === OrgChild4) { if (draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } if (draft.orgChild1Id && parentMappings) { - updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; } if (draft.orgChild2Id && parentMappings) { - updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + updateData.orgChild2Id = + parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; } if (draft.orgChild3Id && parentMappings) { - updateData.orgChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id) ?? draft.orgChild3Id; + updateData.orgChild3Id = + parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id) ?? draft.orgChild3Id; } } @@ -8304,21 +8322,21 @@ export class OrganizationController extends Controller { draftPosMasterId: string, currentPosMasterId: string, draftRevisionId: string, - currentRevisionId: string + currentRevisionId: string, ): Promise { // Fetch draft and current positions for this posMaster const [draftPositions, currentPositions] = await Promise.all([ queryRunner.manager.find(Position, { where: { - posMasterId: draftPosMasterId + posMasterId: draftPosMasterId, }, - order: { orderNo: 'ASC' } + order: { orderNo: "ASC" }, }), queryRunner.manager.find(Position, { where: { - posMasterId: currentPosMasterId - } - }) + posMasterId: currentPosMasterId, + }, + }), ]); // If no draft positions, delete all current positions @@ -8326,16 +8344,14 @@ export class OrganizationController extends Controller { if (currentPositions.length > 0) { await queryRunner.manager.delete( Position, - currentPositions.map((p: any) => p.id) + currentPositions.map((p: any) => p.id), ); } return; } // Build maps for tracking - const currentByOrderNo = new Map( - currentPositions.map((p: any) => [p.orderNo, p]) - ); + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); // DELETE: Current positions not in draft (by orderNo) const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); @@ -8344,7 +8360,7 @@ export class OrganizationController extends Controller { if (toDelete.length > 0) { await queryRunner.manager.delete( Position, - toDelete.map((p: any) => p.id) + toDelete.map((p: any) => p.id), ); } From 47f7f4d55ea8d79d135973c4a098bd30f12e3f9a Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 9 Feb 2026 17:50:53 +0700 Subject: [PATCH 059/178] optimize sort #2260 --- src/controllers/OrganizationController.ts | 233 +++++++++++++++------- 1 file changed, 166 insertions(+), 67 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index e9e4dc5d..39602bc7 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7149,75 +7149,174 @@ export class OrganizationController extends Controller { */ @Get("root/search/sort") async searchSortRootLevelType(@Request() request: RequestWithUser) { - const root1 = await this.orgRootRepository.find({ - where: { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - DEPARTMENT_CODE: Not("50"), - }, - order: { isDeputy: "DESC", orgRootOrder: "ASC" }, - select: ["orgRootName"], - }); - const root2 = await this.orgRootRepository.find({ - where: { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - DEPARTMENT_CODE: "50", - }, - order: { orgRootName: "ASC" }, - select: ["orgRootName"], - }); - const root = [...root1, ...root2]; + // const root1 = await this.orgRootRepository.find({ + // where: { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // DEPARTMENT_CODE: Not("50"), + // }, + // order: { isDeputy: "DESC", orgRootOrder: "ASC" }, + // select: ["orgRootName"], + // }); + // const root2 = await this.orgRootRepository.find({ + // where: { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // DEPARTMENT_CODE: "50", + // }, + // order: { orgRootName: "ASC" }, + // select: ["orgRootName"], + // }); + // const root = [...root1, ...root2]; + + // const child1 = await this.child1Repository.find({ + // where: { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // }, + // order: { orgChild1Order: "ASC" }, + // select: ["orgChild1Name"], + // }); + // const child2 = await this.child2Repository.find({ + // where: { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // }, + // order: { orgChild2Order: "ASC" }, + // select: ["orgChild2Name"], + // }); + // const child3 = await this.child3Repository.find({ + // where: { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // }, + // order: { orgChild3Order: "ASC" }, + // select: ["orgChild3Name"], + // }); + // const child4 = await this.child4Repository.find({ + // where: { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // }, + // order: { orgChild4Order: "ASC" }, + // select: ["orgChild4Name"], + // }); + // const hospital = await this.child1Repository.find({ + // where: [ + // { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // orgRoot: { isDeputy: true }, + // }, + // { + // orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, + // orgChild1RankSub: "HOSPITAL", + // }, + // ], + // select: ["orgChild1Name"], + // }); + // const posType = await this.posTypeRepository.find({ + // order: { posTypeRank: "DESC" }, + // select: ["posTypeName"], + // }); + // const posLevel = await this.posLevelRepository.find({ + // order: { posLevelRank: "DESC" }, + // select: ["posLevelName"], + // }); + + const [ + roots, + child1, + child2, + child3, + child4, + hospital, + posType, + posLevel, + ] = await Promise.all([ + + // ===== ROOT ===== + this.orgRootRepository + .createQueryBuilder("root") + .innerJoin("root.orgRevision", "rev") + .select([ + "root.orgRootName AS orgRootName", + ]) + .where("rev.orgRevisionIsDraft = false") + .andWhere("rev.orgRevisionIsCurrent = true") + .orderBy( + "CASE WHEN root.DEPARTMENT_CODE = '50' THEN 1 ELSE 0 END", + "ASC", + ) + .addOrderBy("root.isDeputy", "DESC") + .addOrderBy("root.orgRootOrder", "ASC") + .addOrderBy("root.orgRootName", "ASC") + .getRawMany(), + + // ===== CHILD 1 ===== + this.child1Repository + .createQueryBuilder("c1") + .innerJoin("c1.orgRevision", "rev") + .select("c1.orgChild1Name", "orgChild1Name") + .where("rev.orgRevisionIsDraft = false") + .andWhere("rev.orgRevisionIsCurrent = true") + .orderBy("c1.orgChild1Order", "ASC") + .getRawMany(), + + // ===== CHILD 2 ===== + this.child2Repository + .createQueryBuilder("c2") + .innerJoin("c2.orgRevision", "rev") + .select("c2.orgChild2Name", "orgChild2Name") + .where("rev.orgRevisionIsDraft = false") + .andWhere("rev.orgRevisionIsCurrent = true") + .orderBy("c2.orgChild2Order", "ASC") + .getRawMany(), + + // ===== CHILD 3 ===== + this.child3Repository + .createQueryBuilder("c3") + .innerJoin("c3.orgRevision", "rev") + .select("c3.orgChild3Name", "orgChild3Name") + .where("rev.orgRevisionIsDraft = false") + .andWhere("rev.orgRevisionIsCurrent = true") + .orderBy("c3.orgChild3Order", "ASC") + .getRawMany(), + + // ===== CHILD 4 ===== + this.child4Repository + .createQueryBuilder("c4") + .innerJoin("c4.orgRevision", "rev") + .select("c4.orgChild4Name", "orgChild4Name") + .where("rev.orgRevisionIsDraft = false") + .andWhere("rev.orgRevisionIsCurrent = true") + .orderBy("c4.orgChild4Order", "ASC") + .getRawMany(), + + // ===== HOSPITAL ===== + this.child1Repository + .createQueryBuilder("c1") + .innerJoin("c1.orgRevision", "rev") + .leftJoin("c1.orgRoot", "root") + .select("c1.orgChild1Name", "orgChild1Name") + .where("rev.orgRevisionIsDraft = false") + .andWhere("rev.orgRevisionIsCurrent = true") + .andWhere( + "(root.isDeputy = true OR c1.orgChild1RankSub = :rank)", + { rank: "HOSPITAL" }, + ) + .getRawMany(), + + // ===== POSITION TYPE ===== + this.posTypeRepository + .createQueryBuilder("pt") + .select("pt.posTypeName", "posTypeName") + .orderBy("pt.posTypeRank", "DESC") + .getRawMany(), + + // ===== POSITION LEVEL ===== + this.posLevelRepository + .createQueryBuilder("pl") + .select("pl.posLevelName", "posLevelName") + .orderBy("pl.posLevelRank", "DESC") + .getRawMany(), + ]); - const child1 = await this.child1Repository.find({ - where: { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - }, - order: { orgChild1Order: "ASC" }, - select: ["orgChild1Name"], - }); - const child2 = await this.child2Repository.find({ - where: { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - }, - order: { orgChild2Order: "ASC" }, - select: ["orgChild2Name"], - }); - const child3 = await this.child3Repository.find({ - where: { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - }, - order: { orgChild3Order: "ASC" }, - select: ["orgChild3Name"], - }); - const child4 = await this.child4Repository.find({ - where: { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - }, - order: { orgChild4Order: "ASC" }, - select: ["orgChild4Name"], - }); - const hospital = await this.child1Repository.find({ - where: [ - { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - orgRoot: { isDeputy: true }, - }, - { - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - orgChild1RankSub: "HOSPITAL", - }, - ], - select: ["orgChild1Name"], - }); - const posType = await this.posTypeRepository.find({ - order: { posTypeRank: "DESC" }, - select: ["posTypeName"], - }); - const posLevel = await this.posLevelRepository.find({ - order: { posLevelRank: "DESC" }, - select: ["posLevelName"], - }); return new HttpSuccess({ - root: root.map((x) => x.orgRootName), + root: roots.map((x) => x.orgRootName), child1: child1.map((x) => x.orgChild1Name), child2: child2.map((x) => x.orgChild2Name), child3: child3.map((x) => x.orgChild3Name), From d6bb9be93d4565b759f4aa5770927f81a04dad55 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 10 Feb 2026 10:44:58 +0700 Subject: [PATCH 060/178] =?UTF-8?q?comment=20=E0=B8=AB=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=E0=B8=A1=E0=B8=A5=E0=B8=9A=E0=B9=80=E0=B8=88=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2=E0=B8=97=E0=B8=B5=E0=B9=88?= =?UTF-8?q?=E0=B8=A5=E0=B8=B3=E0=B8=94=E0=B8=B1=E0=B8=9A=E0=B8=97=E0=B8=B5?= =?UTF-8?q?=E0=B9=88=201=20#2220?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandOperatorController.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/CommandOperatorController.ts b/src/controllers/CommandOperatorController.ts index f1cac92a..1a461ab3 100644 --- a/src/controllers/CommandOperatorController.ts +++ b/src/controllers/CommandOperatorController.ts @@ -187,13 +187,13 @@ export class CommandOperatorController extends Controller { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบเจ้าหน้าที่ดำเนินการ"); } - // 2. ห้ามลบ orderNo = 1 - if (operator.orderNo === 1) { - throw new HttpError( - HttpStatusCode.BAD_REQUEST, - "ไม่สามารถลบเจ้าหน้าที่ลำดับที่ 1 ได้" - ); - } + // // 2. ห้ามลบ orderNo = 1 + // if (operator.orderNo === 1) { + // throw new HttpError( + // HttpStatusCode.BAD_REQUEST, + // "ไม่สามารถลบเจ้าหน้าที่ลำดับที่ 1 ได้" + // ); + // } const removedOrderNo = operator.orderNo; From ecfb65e159301bf72c97c35531a78adbf4583ee5 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 10 Feb 2026 11:33:50 +0700 Subject: [PATCH 061/178] Fix Script #2292 --- src/controllers/ProfileSalaryTempController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 6ea1713c..7835d4cf 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1431,7 +1431,7 @@ export class ProfileSalaryTempController extends Controller { profileEmployeeId: x.profileEmployeeId, dateStart: x.commandDateAffect, dateEnd: null, - posNo: x.posNo, + posNo: `${x.posNoAbb} ${x.posNo}`, position: x.positionName, commandId: x.commandId, refCommandNo: x.commandNo, @@ -1455,7 +1455,7 @@ export class ProfileSalaryTempController extends Controller { dateEnd: null, commandId: x.commandId, commandNo: x.commandNo, - commandName: x.commandName, + commandName: x.commandName ?? "ให้ช่วยราชการ", refCommandDate: x.commandDateSign, refId: x.refId, status: "DONE", From 19d7799b5a23ac479e267cb8557ef484de63b04f Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 10 Feb 2026 13:19:09 +0700 Subject: [PATCH 062/178] Fix Script #2292 --- src/controllers/ProfileSalaryTempController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 7835d4cf..33d6a835 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -1434,7 +1434,7 @@ export class ProfileSalaryTempController extends Controller { posNo: `${x.posNoAbb} ${x.posNo}`, position: x.positionName, commandId: x.commandId, - refCommandNo: x.commandNo, + refCommandNo: `${x.commandNo}/${x.commandYear}`, refCommandDate: x.commandDateAffect, status: false, isDeleted: false, @@ -1454,7 +1454,7 @@ export class ProfileSalaryTempController extends Controller { dateStart: x.commandDateAffect, dateEnd: null, commandId: x.commandId, - commandNo: x.commandNo, + commandNo: `${x.commandNo}/${x.commandYear}`, commandName: x.commandName ?? "ให้ช่วยราชการ", refCommandDate: x.commandDateSign, refId: x.refId, From 3b97e52bd68d4b0bb95a03a7f673c77dce1250e0 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Tue, 10 Feb 2026 16:23:52 +0700 Subject: [PATCH 063/178] complete script move draf to current --- src/controllers/OrganizationController.ts | 279 +++++++++++++++------- 1 file changed, 199 insertions(+), 80 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 02e35abf..83e5719c 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7865,7 +7865,6 @@ export class OrganizationController extends Controller { ]); if (!orgRootDraft) return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้างร่าง"); - // Part 1: Differential sync of organization structure (bottom-up) // Build mapping incrementally as we process each level const allMappings: AllOrgMappings = { @@ -7876,60 +7875,80 @@ export class OrganizationController extends Controller { orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, }; - // Process from bottom (Child4) to top (Root) to handle foreign key constraints - // Child4 (leaf nodes - no children depending on them) - allMappings.orgChild4 = await this.syncOrgLevel( - queryRunner, - OrgChild4, - this.child4Repository, - drafRevisionId, - currentRevisionId, - rootDnaId, - allMappings, - ); + // Track sync statistics for organization nodes + const orgSyncStats: Record = + {}; - // Child3 - allMappings.orgChild3 = await this.syncOrgLevel( - queryRunner, - OrgChild3, - this.child3Repository, - drafRevisionId, - currentRevisionId, - rootDnaId, - allMappings, - ); - - // Child2 - allMappings.orgChild2 = await this.syncOrgLevel( - queryRunner, - OrgChild2, - this.child2Repository, - drafRevisionId, - currentRevisionId, - rootDnaId, - allMappings, - ); - - // Child1 - allMappings.orgChild1 = await this.syncOrgLevel( - queryRunner, - OrgChild1, - this.child1Repository, - drafRevisionId, - currentRevisionId, - rootDnaId, - allMappings, - ); - - // OrgRoot (root level - no parent mapping needed) - allMappings.orgRoot = await this.syncOrgLevel( + // Process from top (Root) to bottom (Child4) to handle foreign key constraints + // OrgRoot (sync first - no parent dependencies) + const orgRootResult = await this.syncOrgLevel( queryRunner, OrgRoot, this.orgRootRepository, drafRevisionId, currentRevisionId, - rootDnaId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, ); + allMappings.orgRoot = orgRootResult.mapping; + orgSyncStats.orgRoot = orgRootResult.counts; + + // Child1 (parent OrgRoot already synced) + const child1Result = await this.syncOrgLevel( + queryRunner, + OrgChild1, + this.child1Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild1 = child1Result.mapping; + orgSyncStats.orgChild1 = child1Result.counts; + + // Child2 (parents OrgRoot and Child1 already synced) + const child2Result = await this.syncOrgLevel( + queryRunner, + OrgChild2, + this.child2Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild2 = child2Result.mapping; + orgSyncStats.orgChild2 = child2Result.counts; + + // Child3 (parents OrgRoot, Child1, Child2 already synced) + const child3Result = await this.syncOrgLevel( + queryRunner, + OrgChild3, + this.child3Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild3 = child3Result.mapping; + orgSyncStats.orgChild3 = child3Result.counts; + + // Child4 (parents OrgRoot, Child1, Child2, Child3 already synced) + const child4Result = await this.syncOrgLevel( + queryRunner, + OrgChild4, + this.child4Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild4 = child4Result.mapping; + orgSyncStats.orgChild4 = child4Result.counts; // Part 2: Sync position data using new org IDs from Part 1 // 2.1 Clear current_holderId for affected positions (keep existing logic) @@ -8097,17 +8116,44 @@ export class OrganizationController extends Controller { } // 2.5 Sync positions table for all affected posMasters + const positionSyncStats: { deleted: number; updated: number; inserted: number } = { + deleted: 0, + updated: 0, + inserted: 0, + }; for (const [draftPosMasterId, currentPosMasterId] of posMasterMapping) { - await this.syncPositionsForPosMaster( + const stats = await this.syncPositionsForPosMaster( queryRunner, draftPosMasterId, currentPosMasterId, drafRevisionId, currentRevisionId, ); + positionSyncStats.deleted += stats.deleted; + positionSyncStats.updated += stats.updated; + positionSyncStats.inserted += stats.inserted; } + // Build comprehensive summary + const summary = { + message: "ย้ายโครงสร้างสำเร็จ", + organization: { + orgRoot: orgSyncStats.orgRoot, + orgChild1: orgSyncStats.orgChild1, + orgChild2: orgSyncStats.orgChild2, + orgChild3: orgSyncStats.orgChild3, + orgChild4: orgSyncStats.orgChild4, + }, + positionMaster: { + deleted: toDelete.length, + updated: toUpdate.length, + inserted: toInsert.length, + }, + position: positionSyncStats, + }; + await queryRunner.commitTransaction(); + return new HttpSuccess(summary); } catch (error) { console.error("Error moving draft to current:", error); await queryRunner.rollbackTransaction(); @@ -8161,23 +8207,49 @@ export class OrganizationController extends Controller { repository: any, draftRevisionId: string, currentRevisionId: string, - rootDnaId: string, parentMappings?: AllOrgMappings, - ): Promise { + draftOrgRootId?: string, + currentOrgRootId?: string, + ): Promise<{ + mapping: OrgIdMapping; + counts: { deleted: number; updated: number; inserted: number }; + }> { // 1. Fetch draft and current nodes under the given rootDnaId + // Build WHERE condition for draft nodes + const draftWhere: any = { + orgRevisionId: draftRevisionId, + }; + if ( + draftOrgRootId && + (entityClass === OrgChild1 || + entityClass === OrgChild2 || + entityClass === OrgChild3 || + entityClass === OrgChild4) + ) { + draftWhere.orgRootId = draftOrgRootId; + } else if (entityClass === OrgRoot) { + draftWhere.id = draftOrgRootId; + } + + // Build WHERE condition for current nodes + const currentWhere: any = { + orgRevisionId: currentRevisionId, + }; + if ( + currentOrgRootId && + (entityClass === OrgChild1 || + entityClass === OrgChild2 || + entityClass === OrgChild3 || + entityClass === OrgChild4) + ) { + currentWhere.orgRootId = currentOrgRootId; + } else if (entityClass === OrgRoot) { + currentWhere.id = currentOrgRootId; + } + const [draftNodes, currentNodes] = await Promise.all([ - repository.find({ - where: { - orgRevisionId: draftRevisionId, - ancestorDNA: Like(`${rootDnaId}%`), - }, - }), - repository.find({ - where: { - orgRevisionId: currentRevisionId, - ancestorDNA: Like(`${rootDnaId}%`), - }, - }), + repository.find({ where: draftWhere }), + repository.find({ where: currentWhere }), ]); // 2. Build lookup maps for efficient matching by ancestorDNA @@ -8270,37 +8342,71 @@ export class OrganizationController extends Controller { }); // Map parent IDs based on entity level - if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) { - newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); - } else if (entityClass === OrgChild2) { - if (draft.orgRootId && parentMappings) { + if (entityClass === OrgChild1 && draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); } + } else if (entityClass === OrgChild2) { + if (draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + } if (draft.orgChild1Id && parentMappings) { - newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + const mappedChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + if (mappedChild1Id) { + newNode.orgChild1Id = mappedChild1Id; + } } } else if (entityClass === OrgChild3) { - if (draft.orgRootId && parentMappings) { - newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + if (draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } } if (draft.orgChild1Id && parentMappings) { - newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + const mappedChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + if (mappedChild1Id) { + newNode.orgChild1Id = mappedChild1Id; + } } if (draft.orgChild2Id && parentMappings) { - newNode.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + const mappedChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + if (mappedChild2Id) { + newNode.orgChild2Id = mappedChild2Id; + } } } else if (entityClass === OrgChild4) { - if (draft.orgRootId && parentMappings) { - newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + if (draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } } if (draft.orgChild1Id && parentMappings) { - newNode.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + const mappedChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + if (mappedChild1Id) { + newNode.orgChild1Id = mappedChild1Id; + } } if (draft.orgChild2Id && parentMappings) { - newNode.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + const mappedChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + if (mappedChild2Id) { + newNode.orgChild2Id = mappedChild2Id; + } } if (draft.orgChild3Id && parentMappings) { - newNode.orgChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id); + const mappedChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id); + if (mappedChild3Id) { + newNode.orgChild3Id = mappedChild3Id; + } } } @@ -8310,7 +8416,14 @@ export class OrganizationController extends Controller { mapping.byDraftId.set(draft.id, saved.id); } - return mapping; + return { + mapping, + counts: { + deleted: toDelete.length, + updated: toUpdate.length, + inserted: toInsert.length, + }, + }; } /** @@ -8323,7 +8436,7 @@ export class OrganizationController extends Controller { currentPosMasterId: string, draftRevisionId: string, currentRevisionId: string, - ): Promise { + ): Promise<{ deleted: number; updated: number; inserted: number }> { // Fetch draft and current positions for this posMaster const [draftPositions, currentPositions] = await Promise.all([ queryRunner.manager.find(Position, { @@ -8347,7 +8460,7 @@ export class OrganizationController extends Controller { currentPositions.map((p: any) => p.id), ); } - return; + return { deleted: currentPositions.length, updated: 0, inserted: 0 }; } // Build maps for tracking @@ -8365,6 +8478,8 @@ export class OrganizationController extends Controller { } // UPDATE and INSERT + let updatedCount = 0; + let insertedCount = 0; for (const draftPos of draftPositions) { const current: any = currentByOrderNo.get(draftPos.orderNo); @@ -8380,6 +8495,7 @@ export class OrganizationController extends Controller { positionArea: draftPos.positionArea, isSpecial: draftPos.isSpecial, }); + updatedCount++; } else { // INSERT new position const newPosition = queryRunner.manager.create(Position, { @@ -8388,7 +8504,10 @@ export class OrganizationController extends Controller { posMasterId: currentPosMasterId, }); await queryRunner.manager.save(newPosition); + insertedCount++; } } + + return { deleted: toDelete.length, updated: updatedCount, inserted: insertedCount }; } } From 520b42f2c7373bba1ebf58a242710f7f1f7881b0 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Tue, 10 Feb 2026 17:43:01 +0700 Subject: [PATCH 064/178] fix: script update profile --- src/controllers/OrganizationController.ts | 50 +++++++++++------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index aaa1e7a4..992fb053 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7219,30 +7219,15 @@ export class OrganizationController extends Controller { // select: ["posLevelName"], // }); - const [ - roots, - child1, - child2, - child3, - child4, - hospital, - posType, - posLevel, - ] = await Promise.all([ - + const [roots, child1, child2, child3, child4, hospital, posType, posLevel] = await Promise.all([ // ===== ROOT ===== this.orgRootRepository .createQueryBuilder("root") .innerJoin("root.orgRevision", "rev") - .select([ - "root.orgRootName AS orgRootName", - ]) + .select(["root.orgRootName AS orgRootName"]) .where("rev.orgRevisionIsDraft = false") .andWhere("rev.orgRevisionIsCurrent = true") - .orderBy( - "CASE WHEN root.DEPARTMENT_CODE = '50' THEN 1 ELSE 0 END", - "ASC", - ) + .orderBy("CASE WHEN root.DEPARTMENT_CODE = '50' THEN 1 ELSE 0 END", "ASC") .addOrderBy("root.isDeputy", "DESC") .addOrderBy("root.orgRootOrder", "ASC") .addOrderBy("root.orgRootName", "ASC") @@ -7296,10 +7281,7 @@ export class OrganizationController extends Controller { .select("c1.orgChild1Name", "orgChild1Name") .where("rev.orgRevisionIsDraft = false") .andWhere("rev.orgRevisionIsCurrent = true") - .andWhere( - "(root.isDeputy = true OR c1.orgChild1RankSub = :rank)", - { rank: "HOSPITAL" }, - ) + .andWhere("(root.isDeputy = true OR c1.orgChild1RankSub = :rank)", { rank: "HOSPITAL" }) .getRawMany(), // ===== POSITION TYPE ===== @@ -8133,7 +8115,8 @@ export class OrganizationController extends Controller { const toInsert: any[] = []; // Track draft PosMaster ID to current PosMaster ID mapping for position sync - const posMasterMapping: Map = new Map(); + // Type: Map + const posMasterMapping: Map = new Map(); for (const draftPos of posMasterDraft) { const current = currentByDNA.get(draftPos.ancestorDNA); @@ -8176,7 +8159,7 @@ export class OrganizationController extends Controller { toUpdate.push(current); // Track mapping for position sync - posMasterMapping.set(draftPos.id, current.id); + posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); } else { // INSERT new position const newPosMaster = queryRunner.manager.create(PosMaster, { @@ -8208,7 +8191,7 @@ export class OrganizationController extends Controller { for (let i = 0; i < saved.length; i++) { const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; if (draftPos && saved[i]) { - posMasterMapping.set(draftPos.id, saved[i].id); + posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]); } } } @@ -8220,13 +8203,14 @@ export class OrganizationController extends Controller { updated: 0, inserted: 0, }; - for (const [draftPosMasterId, currentPosMasterId] of posMasterMapping) { + for (const [draftPosMasterId, [currentPosMasterId, nextHolderId]] of posMasterMapping) { const stats = await this.syncPositionsForPosMaster( queryRunner, draftPosMasterId, currentPosMasterId, drafRevisionId, currentRevisionId, + nextHolderId, ); positionSyncStats.deleted += stats.deleted; positionSyncStats.updated += stats.updated; @@ -8535,6 +8519,7 @@ export class OrganizationController extends Controller { currentPosMasterId: string, draftRevisionId: string, currentRevisionId: string, + nextHolderId: string | null | undefined, ): Promise<{ deleted: number; updated: number; inserted: number }> { // Fetch draft and current positions for this posMaster const [draftPositions, currentPositions] = await Promise.all([ @@ -8593,6 +8578,10 @@ export class OrganizationController extends Controller { positionExecutiveField: draftPos.positionExecutiveField, positionArea: draftPos.positionArea, isSpecial: draftPos.isSpecial, + orderNo: draftPos.orderNo, + positionIsSelected: draftPos.positionIsSelected, + lastUpdateFullName: draftPos.lastUpdateFullName, + lastUpdatedAt: new Date(), }); updatedCount++; } else { @@ -8605,6 +8594,15 @@ export class OrganizationController extends Controller { await queryRunner.manager.save(newPosition); insertedCount++; } + + // update profile + if (nextHolderId != null && draftPos.positionIsSelected) { + await queryRunner.manager.update(Profile, nextHolderId, { + position: draftPos.positionName, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + }); + } } return { deleted: toDelete.length, updated: updatedCount, inserted: insertedCount }; From 4f900ba4d2af57878b5fc316c59bac0504c2965f Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 11 Feb 2026 09:47:45 +0700 Subject: [PATCH 065/178] no message --- src/controllers/OrganizationController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 992fb053..70c024cc 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -1899,6 +1899,7 @@ export class OrganizationController extends Controller { const formattedData = await Promise.all( orgRootData.map(async (orgRoot) => { return { + orgDnaId: orgRoot.ancestorDNA, orgTreeId: orgRoot.id, orgLevel: 0, misId: orgRoot.misId, From 5bee36028066527082561024e451d4a6ac6418b2 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 11 Feb 2026 09:52:35 +0700 Subject: [PATCH 066/178] add orgDnaId --- src/controllers/OrganizationController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 70c024cc..b2a5c520 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -1713,6 +1713,7 @@ export class OrganizationController extends Controller { }, ) .select([ + "orgRoot.ancestorDNA", "orgRoot.id", "orgRoot.misId", "orgRoot.isDeputy", From 073da70a68a5172715c4a4ecc84daa3dc4b05011 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 11 Feb 2026 10:40:01 +0700 Subject: [PATCH 067/178] move isProbatin --- src/controllers/OrganizationDotnetController.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 0a2f40e0..1d4ef30a 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -1770,7 +1770,6 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, - isProbatin: profile.isProbation, posType: profile.posType?.posTypeName ?? null, posLevel: profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null @@ -1886,7 +1885,6 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, - isProbatin: profile.isProbation, posLevel: profile.posLevel?.posLevelName ?? null, posType: profile.posType?.posTypeName ?? null, @@ -2052,7 +2050,6 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, - isProbatin: profile.isProbation, posType: profile.posType?.posTypeName ?? null, posLevel: profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null @@ -2218,7 +2215,6 @@ export class OrganizationDotnetController extends Controller { amount: profile.amount, positionSalaryAmount: profile.positionSalaryAmount, mouthSalaryAmount: profile.mouthSalaryAmount, - isProbatin: profile.isProbation, posLevel: profile.posLevel?.posLevelName ?? null, posType: profile.posType?.posTypeName ?? null, From a3d9d40a529d3c379f7e7475c7f95128830f4914 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 11 Feb 2026 11:31:09 +0700 Subject: [PATCH 068/178] api getProfile (No token) --- .../OrganizationDotnetController.ts | 61 +- .../OrganizationUnauthorizeController.ts | 676 +++++++++++++++++- 2 files changed, 707 insertions(+), 30 deletions(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 1d4ef30a..16566685 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -2345,8 +2345,6 @@ export class OrganizationDotnetController extends Controller { relations: [ "posLevel", "posType", - "profileSalary", - "profileInsignias", "current_holders", "current_holders.orgRevision", "current_holders.orgRoot", @@ -2356,14 +2354,6 @@ export class OrganizationDotnetController extends Controller { "current_holders.orgChild4", ], where: { id: profileId }, - order: { - profileSalary: { - commandDateAffect: "DESC", - }, - profileInsignias: { - receiveDate: "DESC", - }, - }, }); if (!profile) { @@ -2371,8 +2361,6 @@ export class OrganizationDotnetController extends Controller { relations: [ "posLevel", "posType", - "profileSalary", - "profileInsignias", "current_holders", "current_holders.orgRevision", "current_holders.orgRoot", @@ -2382,17 +2370,20 @@ export class OrganizationDotnetController extends Controller { "current_holders.orgChild4", ], where: { id: profileId }, - order: { - profileSalary: { - commandDateAffect: "DESC", - }, - profileInsignias: { - receiveDate: "DESC", - }, - }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const [latestSalary, latestInsignia] = await Promise.all([ + this.salaryRepo.findOne({ + where: { profileEmployeeId: profile.id }, + order: { commandDateAffect: "DESC" }, + }), + this.insigniaRepo.findOne({ + where: { profileEmployeeId: profile.id }, + order: { receiveDate: "DESC" }, + }), + ]); + const org = { root: profile?.current_holders?.find( @@ -2426,7 +2417,7 @@ export class OrganizationDotnetController extends Controller { )?.orgChild4?.id ?? null, }; let fullname = ""; - let pos = await this.posMasterRepository.findOne({ + let pos = await this.empPosMasterRepository.findOne({ relations: ["current_holder"], where: { current_holder: Not(IsNull()), @@ -2450,7 +2441,7 @@ export class OrganizationDotnetController extends Controller { " " + pos.current_holder.lastName; } else { - let pos = await this.posMasterRepository.findOne({ + let pos = await this.empPosMasterRepository.findOne({ relations: ["current_holder"], where: { current_holder: Not(IsNull()), @@ -2473,7 +2464,7 @@ export class OrganizationDotnetController extends Controller { " " + pos.current_holder.lastName; } else { - let pos = await this.posMasterRepository.findOne({ + let pos = await this.empPosMasterRepository.findOne({ relations: ["current_holder"], where: { current_holder: Not(IsNull()), @@ -2496,7 +2487,7 @@ export class OrganizationDotnetController extends Controller { " " + pos.current_holder.lastName; } else { - let pos = await this.posMasterRepository.findOne({ + let pos = await this.empPosMasterRepository.findOne({ relations: ["current_holder"], where: { current_holder: Not(IsNull()), @@ -2519,7 +2510,7 @@ export class OrganizationDotnetController extends Controller { " " + pos.current_holder.lastName; } else { - let pos = await this.posMasterRepository.findOne({ + let pos = await this.empPosMasterRepository.findOne({ relations: ["current_holder"], where: { current_holder: Not(IsNull()), @@ -2689,14 +2680,25 @@ export class OrganizationDotnetController extends Controller { ? "" : profile.posType?.posTypeShortName + " ") + (profile.posLevel?.posLevelName ?? ""), posType: profile.posType?.posTypeName ?? null, - profileSalary: profile.profileSalary.length > 0 ? profile.profileSalary[0] : null, - profileInsignia: profile.profileInsignias.length > 0 ? profile.profileInsignias[0] : null, + profileSalary: latestSalary ?? null, + profileInsignia: latestInsignia ?? null, profileType: "EMPLOYEE", }; return new HttpSuccess(mapProfile); } + const [latestSalary, latestInsignia] = await Promise.all([ + this.salaryRepo.findOne({ + where: { profileId: profile.id }, + order: { commandDateAffect: "DESC" }, + }), + this.insigniaRepo.findOne({ + where: { profileId: profile.id }, + order: { receiveDate: "DESC" }, + }), + ]); + const org = { root: profile?.current_holders?.find( @@ -2991,13 +2993,14 @@ export class OrganizationDotnetController extends Controller { commander: fullname, posLevel: profile.posLevel?.posLevelName ?? null, posType: profile.posType?.posTypeName ?? null, - profileSalary: profile.profileSalary.length > 0 ? profile.profileSalary[0] : null, - profileInsignia: profile.profileInsignias.length > 0 ? profile.profileInsignias[0] : null, + profileSalary: latestSalary ?? null, + profileInsignia: latestInsignia ?? null, profileType: "OFFICER", }; return new HttpSuccess(mapProfile); } + /** * 3. API Get Profile จาก citizen Id * diff --git a/src/controllers/OrganizationUnauthorizeController.ts b/src/controllers/OrganizationUnauthorizeController.ts index dfd26df5..d2ec617d 100644 --- a/src/controllers/OrganizationUnauthorizeController.ts +++ b/src/controllers/OrganizationUnauthorizeController.ts @@ -22,6 +22,8 @@ import Extension from "../interfaces/extension"; import { resetPassword } from "../keycloak"; import { viewEmployeePosMaster } from "../entities/view/viewEmployeePosMaster"; import { EmployeePosDict } from "../entities/EmployeePosDict"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileInsignia } from "../entities/ProfileInsignia"; @Route("api/v1/org/unauthorize") @Tags("OrganizationUnauthorize") @Response( @@ -39,7 +41,10 @@ export class OrganizationUnauthorizeController extends Controller { viewProfileEmployeeEvaluation, ); private employeePosDictRepository = AppDataSource.getRepository(EmployeePosDict); - + private posMasterRepository = AppDataSource.getRepository(PosMaster); + private empPosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); + private salaryRepo = AppDataSource.getRepository(ProfileSalary); + private insigniaRepo = AppDataSource.getRepository(ProfileInsignia); @Post("user/reset-password") async forgetPassword( @Body() @@ -1036,6 +1041,675 @@ export class OrganizationUnauthorizeController extends Controller { return new HttpSuccess(findRevision.id); } + /** + * API Get Profile จาก profile id + * + * @summary API Get Profile จาก profile id + * + * @param {string} profileId Id profile + */ + @Get("profile/{profileId}") + async GetProfileByProfileIdAsync(@Path() profileId: string) { + const profile = await this.profileRepo.findOne({ + relations: [ + "posLevel", + "posType", + "current_holders", + "current_holders.orgRevision", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + ], + where: { id: profileId }, + }); + + if (!profile) { + const profile = await this.profileEmpRepo.findOne({ + relations: [ + "posLevel", + "posType", + "current_holders", + "current_holders.orgRevision", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + ], + where: { id: profileId }, + }); + if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + + const [latestSalary, latestInsignia] = await Promise.all([ + this.salaryRepo.findOne({ + where: { profileEmployeeId: profile.id }, + order: { commandDateAffect: "DESC" }, + }), + this.insigniaRepo.findOne({ + where: { profileEmployeeId: profile.id }, + order: { receiveDate: "DESC" }, + }), + ]); + + const org = { + root: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRoot?.id ?? null, + child1: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild1?.id ?? null, + child2: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2?.id ?? null, + child3: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3?.id ?? null, + child4: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4?.id ?? null, + }; + let fullname = ""; + let pos = await this.empPosMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: org?.child2 ?? "", + orgChild3Id: org?.child3 ?? "", + orgChild4Id: org?.child4 ?? "", + }, + }); + + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.empPosMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: org?.child2 ?? "", + orgChild3Id: org?.child3 ?? "", + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.empPosMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: org?.child2 ?? "", + orgChild3Id: IsNull(), + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.empPosMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: IsNull(), + orgChild3Id: IsNull(), + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.empPosMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + orgRootId: org?.root ?? "", + orgChild1Id: IsNull(), + orgChild2Id: IsNull(), + orgChild3Id: IsNull(), + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + fullname = ""; + } + } + } + } + } + + const mapProfile = { + id: profile.id, + avatar: profile.avatar, + avatarName: profile.avatarName, + rank: profile.rank, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + position: profile.position, + posLevelId: profile.posLevelId, + email: profile.email, + phone: profile.phone, + keycloak: profile.keycloak, + isProbation: profile.isProbation, + isLeave: profile.isLeave, + leaveReason: profile.leaveReason, + dateRetire: profile.dateRetire, + dateAppoint: profile.dateAppoint, + dateRetireLaw: profile.dateRetireLaw, + dateStart: profile.dateStart, + govAgeAbsent: profile.govAgeAbsent, + govAgePlus: profile.govAgePlus, + birthDate: profile.birthDate ?? new Date(), + reasonSameDate: profile.reasonSameDate, + telephoneNumber: profile.phone, + nationality: profile.nationality, + gender: profile.gender, + relationship: profile.relationship, + religion: profile.religion, + bloodGroup: profile.bloodGroup, + registrationAddress: profile.registrationAddress, + registrationProvinceId: profile.registrationProvinceId, + registrationDistrictId: profile.registrationDistrictId, + registrationSubDistrictId: profile.registrationSubDistrictId, + registrationZipCode: profile.registrationZipCode, + currentAddress: profile.currentAddress, + currentProvinceId: profile.currentProvinceId, + currentSubDistrictId: profile.currentSubDistrictId, + currentZipCode: profile.currentZipCode, + dutyTimeId: profile.dutyTimeId, + dutyTimeEffectiveDate: profile.dutyTimeEffectiveDate, + amount: profile.amount, + positionSalaryAmount: profile.positionSalaryAmount, + mouthSalaryAmount: profile.mouthSalaryAmount, + root: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRoot?.orgRootName ?? null, + child1: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild1?.orgChild1Name ?? null, + child2: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2?.orgChild2Name ?? null, + child3: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3?.orgChild3Name ?? null, + child4: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4?.orgChild4Name ?? null, + rootId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRootId ?? null, + child1Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild1Id ?? null, + child2Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2Id ?? null, + child3Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3Id ?? null, + child4Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4Id ?? null, + rootDnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRoot?.ancestorDNA ?? null, + child1DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3?.ancestorDNA ?? null, + child2DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2?.ancestorDNA ?? null, + child3DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3?.ancestorDNA ?? null, + child4DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4?.ancestorDNA ?? null, + commander: fullname, + posLevel: + (profile.posType == null || profile.posType?.posTypeShortName == null + ? "" + : profile.posType?.posTypeShortName + " ") + (profile.posLevel?.posLevelName ?? ""), + posType: profile.posType?.posTypeName ?? null, + profileSalary: latestSalary ?? null, + profileInsignia: latestInsignia ?? null, + profileType: "EMPLOYEE", + }; + + return new HttpSuccess(mapProfile); + } + + const [latestSalary, latestInsignia] = await Promise.all([ + this.salaryRepo.findOne({ + where: { profileId: profile.id }, + order: { commandDateAffect: "DESC" }, + }), + this.insigniaRepo.findOne({ + where: { profileId: profile.id }, + order: { receiveDate: "DESC" }, + }), + ]); + + const org = { + root: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRoot?.id ?? null, + child1: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild1?.id ?? null, + child2: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2?.id ?? null, + child3: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3?.id ?? null, + child4: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4?.id ?? null, + }; + + let fullname = ""; + let pos = await this.posMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: org?.child2 ?? "", + orgChild3Id: org?.child3 ?? "", + orgChild4Id: org?.child4 ?? "", + }, + }); + + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.posMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: org?.child2 ?? "", + orgChild3Id: org?.child3 ?? "", + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.posMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: org?.child2 ?? "", + orgChild3Id: IsNull(), + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.posMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + isDirector: true, + orgRootId: org?.root ?? "", + orgChild1Id: org?.child1 ?? "", + orgChild2Id: IsNull(), + orgChild3Id: IsNull(), + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + let pos = await this.posMasterRepository.findOne({ + relations: ["current_holder"], + where: { + current_holder: Not(IsNull()), + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + orgRootId: org?.root ?? "", + orgChild1Id: IsNull(), + orgChild2Id: IsNull(), + orgChild3Id: IsNull(), + orgChild4Id: IsNull(), + }, + }); + if (pos) { + fullname = + pos.current_holder.prefix + + pos.current_holder.firstName + + " " + + pos.current_holder.lastName; + } else { + fullname = ""; + } + } + } + } + } + + const mapProfile = { + id: profile.id, + avatar: profile.avatar, + avatarName: profile.avatarName, + rank: profile.rank, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + position: profile.position, + posLevelId: profile.posLevelId, + email: profile.email, + phone: profile.phone, + keycloak: profile.keycloak, + isProbation: profile.isProbation, + isLeave: profile.isLeave, + leaveReason: profile.leaveReason, + dateRetire: profile.dateRetire, + dateAppoint: profile.dateAppoint, + dateRetireLaw: profile.dateRetireLaw, + dateStart: profile.dateStart, + govAgeAbsent: profile.govAgeAbsent, + govAgePlus: profile.govAgePlus, + birthDate: profile.birthDate ?? new Date(), + reasonSameDate: profile.reasonSameDate, + telephoneNumber: profile.phone, + nationality: profile.nationality, + gender: profile.gender, + relationship: profile.relationship, + religion: profile.religion, + bloodGroup: profile.bloodGroup, + registrationAddress: profile.registrationAddress, + registrationProvinceId: profile.registrationProvinceId, + registrationDistrictId: profile.registrationDistrictId, + registrationSubDistrictId: profile.registrationSubDistrictId, + registrationZipCode: profile.registrationZipCode, + currentAddress: profile.currentAddress, + currentProvinceId: profile.currentProvinceId, + currentSubDistrictId: profile.currentSubDistrictId, + currentZipCode: profile.currentZipCode, + dutyTimeId: profile.dutyTimeId, + dutyTimeEffectiveDate: profile.dutyTimeEffectiveDate, + amount: profile.amount, + positionSalaryAmount: profile.positionSalaryAmount, + mouthSalaryAmount: profile.mouthSalaryAmount, + root: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRoot?.orgRootName ?? null, + child1: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild1?.orgChild1Name ?? null, + child2: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2?.orgChild2Name ?? null, + child3: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3?.orgChild3Name ?? null, + child4: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4?.orgChild4Name ?? null, + rootId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRootId ?? null, + child1Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild1Id ?? null, + child2Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2Id ?? null, + child3Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3Id ?? null, + child4Id: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4Id ?? null, + rootDnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgRoot?.ancestorDNA ?? null, + child1DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild1?.ancestorDNA ?? null, + child2DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild2?.ancestorDNA ?? null, + child3DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild3?.ancestorDNA ?? null, + child4DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft == false && + x.orgRevision?.orgRevisionIsCurrent == true, + )?.orgChild4?.ancestorDNA ?? null, + commander: fullname, + posLevel: profile.posLevel?.posLevelName ?? null, + posType: profile.posType?.posTypeName ?? null, + profileSalary: latestSalary ?? null, + profileInsignia: latestInsignia ?? null, + profileType: "OFFICER", + }; + + return new HttpSuccess(mapProfile); + } + /** * API หา user profile officer * From c5e600900c490abad67cc7a4206fd9beb208df12 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 11 Feb 2026 13:10:42 +0700 Subject: [PATCH 069/178] fix time out --- src/controllers/OrganizationController.ts | 199 +++++++++++++++++++--- 1 file changed, 178 insertions(+), 21 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 992fb053..1b233b4b 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8197,25 +8197,13 @@ export class OrganizationController extends Controller { } } - // 2.5 Sync positions table for all affected posMasters - const positionSyncStats: { deleted: number; updated: number; inserted: number } = { - deleted: 0, - updated: 0, - inserted: 0, - }; - for (const [draftPosMasterId, [currentPosMasterId, nextHolderId]] of posMasterMapping) { - const stats = await this.syncPositionsForPosMaster( - queryRunner, - draftPosMasterId, - currentPosMasterId, - drafRevisionId, - currentRevisionId, - nextHolderId, - ); - positionSyncStats.deleted += stats.deleted; - positionSyncStats.updated += stats.updated; - positionSyncStats.inserted += stats.inserted; - } + // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) + const positionSyncStats = await this.syncAllPositionsBatch( + queryRunner, + posMasterMapping, + drafRevisionId, + currentRevisionId, + ); // Build comprehensive summary const summary = { @@ -8512,13 +8500,15 @@ export class OrganizationController extends Controller { /** * Helper function: Sync positions for a PosMaster * Handles DELETE/UPDATE/INSERT for positions associated with a posMaster + * + * @deprecated Kept as fallback - use syncAllPositionsBatch for better performance */ private async syncPositionsForPosMaster( queryRunner: any, draftPosMasterId: string, currentPosMasterId: string, - draftRevisionId: string, - currentRevisionId: string, + _draftRevisionId: string, + _currentRevisionId: string, nextHolderId: string | null | undefined, ): Promise<{ deleted: number; updated: number; inserted: number }> { // Fetch draft and current positions for this posMaster @@ -8607,4 +8597,171 @@ export class OrganizationController extends Controller { return { deleted: toDelete.length, updated: updatedCount, inserted: insertedCount }; } + + /** + * Batch version: Sync positions for ALL posMasters in a single operation + * This significantly reduces database round trips for large organizations + */ + private async syncAllPositionsBatch( + queryRunner: any, + posMasterMapping: Map, + _draftRevisionId: string, + _currentRevisionId: string, + ): Promise<{ deleted: number; updated: number; inserted: number }> { + // Extract draft and current posMaster IDs + const draftPosMasterIds = Array.from(posMasterMapping.keys()); + const currentPosMasterIds = Array.from(posMasterMapping.values()).map(([currentId]) => currentId); + + if (draftPosMasterIds.length === 0) { + return { deleted: 0, updated: 0, inserted: 0 }; + } + + // Fetch ALL positions for ALL posMasters in just 2 queries + const [allDraftPositions, allCurrentPositions] = await Promise.all([ + queryRunner.manager.find(Position, { + where: { + posMasterId: In(draftPosMasterIds), + }, + order: { orderNo: "ASC" }, + }), + queryRunner.manager.find(Position, { + where: { + posMasterId: In(currentPosMasterIds), + }, + }), + ]); + + // Group positions by posMasterId for processing + const draftPositionsByMaster = new Map(); + for (const pos of allDraftPositions) { + if (!draftPositionsByMaster.has(pos.posMasterId)) { + draftPositionsByMaster.set(pos.posMasterId, []); + } + draftPositionsByMaster.get(pos.posMasterId)!.push(pos); + } + + const currentPositionsByMaster = new Map(); + for (const pos of allCurrentPositions) { + if (!currentPositionsByMaster.has(pos.posMasterId)) { + currentPositionsByMaster.set(pos.posMasterId, []); + } + currentPositionsByMaster.get(pos.posMasterId)!.push(pos); + } + + // Collect all operations + const allToDelete: string[] = []; + const allToUpdate: Array<{ id: string; data: any }> = []; + const allToInsert: Array = []; + const profileUpdates: Map = new Map(); + + // Process each posMaster mapping + for (const [draftPosMasterId, [currentPosMasterId, nextHolderId]] of posMasterMapping) { + const draftPositions = draftPositionsByMaster.get(draftPosMasterId) || []; + const currentPositions = currentPositionsByMaster.get(currentPosMasterId) || []; + + // If no draft positions, mark all current positions for deletion + if (draftPositions.length === 0) { + allToDelete.push(...currentPositions.map((p: any) => p.id)); + continue; + } + + // Build map for tracking + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); + + // Mark for deletion: current positions not in draft (by orderNo) + for (const currentPos of currentPositions) { + if (!draftOrderNos.has(currentPos.orderNo)) { + allToDelete.push(currentPos.id); + } + } + + // Process UPDATE and INSERT + for (const draftPos of draftPositions) { + const current = currentByOrderNo.get(draftPos.orderNo); + + if (current) { + // UPDATE existing position - collect for batch update + allToUpdate.push({ + id: current.id, + data: { + positionName: draftPos.positionName, + positionField: draftPos.positionField, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + posExecutiveId: draftPos.posExecutiveId, + positionExecutiveField: draftPos.positionExecutiveField, + positionArea: draftPos.positionArea, + isSpecial: draftPos.isSpecial, + orderNo: draftPos.orderNo, + positionIsSelected: draftPos.positionIsSelected, + lastUpdateFullName: draftPos.lastUpdateFullName, + lastUpdatedAt: new Date(), + }, + }); + } else { + // INSERT new position - collect for batch insert + allToInsert.push({ + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }); + } + + // Collect profile update for the selected position + if (nextHolderId != null && draftPos.positionIsSelected) { + profileUpdates.set(nextHolderId, { + position: draftPos.positionName, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + }); + } + } + } + + // Execute bulk operations + let deletedCount = 0; + let updatedCount = 0; + let insertedCount = 0; + + // Bulk DELETE + if (allToDelete.length > 0) { + await queryRunner.manager.delete(Position, allToDelete); + deletedCount = allToDelete.length; + } + + // Bulk UPDATE (batch by 500 to avoid query size limits) + if (allToUpdate.length > 0) { + const batchSize = 500; + for (let i = 0; i < allToUpdate.length; i += batchSize) { + const batch = allToUpdate.slice(i, i + batchSize); + await Promise.all( + batch.map(({ id, data }) => queryRunner.manager.update(Position, id, data)) + ); + } + updatedCount = allToUpdate.length; + } + + // Bulk INSERT + if (allToInsert.length > 0) { + const batchSize = 500; + for (let i = 0; i < allToInsert.length; i += batchSize) { + const batch = allToInsert.slice(i, i + batchSize); + await queryRunner.manager.save(Position, batch); + } + insertedCount = allToInsert.length; + } + + // Bulk UPDATE profiles + if (profileUpdates.size > 0) { + const profileUpdateEntries = Array.from(profileUpdates.entries()); + await Promise.all( + profileUpdateEntries.map(([profileId, data]) => + queryRunner.manager.update(Profile, profileId, data) + ) + ); + } + + return { deleted: deletedCount, updated: updatedCount, inserted: insertedCount }; + } } From 1f809d3e22739b055b44d54d898845e3ec46b4d1 Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 11 Feb 2026 15:13:40 +0700 Subject: [PATCH 070/178] fix: #2234 --- src/controllers/PositionController.ts | 148 ++++++++++++++++---------- 1 file changed, 92 insertions(+), 56 deletions(-) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index d8aee369..d8e9bd7e 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -38,7 +38,7 @@ import { EmployeePosLevel } from "../entities/EmployeePosLevel"; import { AuthRole } from "../entities/AuthRole"; import { RequestWithUser } from "../middlewares/user"; import permission from "../interfaces/permission"; -import { setLogDataDiff } from "../interfaces/utils"; +import { resolveNodeLevel, setLogDataDiff } from "../interfaces/utils"; import { PosMasterAssign } from "../entities/PosMasterAssign"; import { Assign } from "../entities/Assign"; import { ProfileEmployee } from "../entities/ProfileEmployee"; @@ -5272,9 +5272,45 @@ export class PositionController extends Controller { let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`; let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`; let _data = await new permission().PermissionOrgList(request, "SYS_POS_CONDITION"); + const orgDna = await new permission().checkDna(request, request.user.sub); + let level: any = resolveNodeLevel(orgDna); + + const cannotViewRootPosMaster = + (_data.privilege === "ROOT" && level > 0) || + (_data.privilege === "PARENT") || + (_data.privilege === "BROTHER" && level > 1) || + (_data.privilege === "CHILD" && level > 0) || + (_data.privilege === "NORMAL" && level != 0); + + const cannotViewChild1PosMaster = + (_data.privilege === "ROOT" && level > 1) || + (_data.privilege === "PARENT" && level > 1) || + (_data.privilege === "BROTHER" && level > 2) || + (_data.privilege === "CHILD" && level > 1) || + (_data.privilege === "NORMAL" && level !== 1); + + const cannotViewChild2PosMaster = + (_data.privilege === "ROOT" && level > 2) || + (_data.privilege === "PARENT" && level > 2) || + (_data.privilege === "BROTHER" && level > 3) || + (_data.privilege === "CHILD" && level > 2) || + (_data.privilege === "NORMAL" && level !== 2); + + const cannotViewChild3PosMaster = + (_data.privilege === "ROOT" && level > 3) || + (_data.privilege === "PARENT" && level > 3) || + (_data.privilege === "BROTHER" && level > 4) || + (_data.privilege === "CHILD" && level > 3) || + (_data.privilege === "NORMAL" && level !== 3); + + const cannotViewChild4PosMaster = + (_data.privilege === "PARENT" && level > 4) || + (_data.privilege === "CHILD" && level > 4) || + (_data.privilege === "NORMAL" && level !== 4); + if (body.type === 0) { typeCondition = { - orgRootId: body.id, + ...(cannotViewRootPosMaster ? { orgRootId: null } : { orgRootId: body.id }), }; if (!body.isAll) { checkChildConditions = { @@ -5285,7 +5321,7 @@ export class PositionController extends Controller { } } else if (body.type === 1) { typeCondition = { - orgChild1Id: body.id, + ...(cannotViewChild1PosMaster ? { orgChild1Id: null } : { orgChild1Id: body.id }), }; if (!body.isAll) { checkChildConditions = { @@ -5296,7 +5332,7 @@ export class PositionController extends Controller { } } else if (body.type === 2) { typeCondition = { - orgChild2Id: body.id, + ...(cannotViewChild2PosMaster ? { orgChild2Id: null } : { orgChild2Id: body.id }), }; if (!body.isAll) { checkChildConditions = { @@ -5307,7 +5343,7 @@ export class PositionController extends Controller { } } else if (body.type === 3) { typeCondition = { - orgChild3Id: body.id, + ...(cannotViewChild3PosMaster ? { orgChild3Id: null } : { orgChild3Id: body.id }), }; if (!body.isAll) { checkChildConditions = { @@ -5318,7 +5354,7 @@ export class PositionController extends Controller { } } else if (body.type === 4) { typeCondition = { - orgChild4Id: body.id, + ...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }), }; searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; } @@ -5403,56 +5439,56 @@ export class PositionController extends Controller { .leftJoinAndSelect("current_holder.posType", "posType") .leftJoinAndSelect("current_holder.posLevel", "posLevel") .where(conditions) - .andWhere( - _data.root != undefined && _data.root != null - ? _data.root[0] != null - ? `posMaster.orgRootId IN (:...root)` - : `posMaster.orgRootId is null` - : "1=1", - { - 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"}` - : "1=1", - { - child1: _data.child1, - }, - ) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `posMaster.orgChild2Id IN (:...child2)` - : `posMaster.orgChild2Id is null` - : "1=1", - { - child2: _data.child2, - }, - ) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `posMaster.orgChild3Id IN (:...child3)` - : `posMaster.orgChild3Id is null` - : "1=1", - { - child3: _data.child3, - }, - ) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `posMaster.orgChild4Id IN (:...child4)` - : `posMaster.orgChild4Id is null` - : "1=1", - { - child4: _data.child4, - }, - ) + // .andWhere( + // _data.root != undefined && _data.root != null + // ? _data.root[0] != null + // ? `posMaster.orgRootId IN (:...root)` + // : `posMaster.orgRootId is null` + // : "1=1", + // { + // 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"}` + // : "1=1", + // { + // child1: _data.child1, + // }, + // ) + // .andWhere( + // _data.child2 != undefined && _data.child2 != null + // ? _data.child2[0] != null + // ? `posMaster.orgChild2Id IN (:...child2)` + // : `posMaster.orgChild2Id is null` + // : "1=1", + // { + // child2: _data.child2, + // }, + // ) + // .andWhere( + // _data.child3 != undefined && _data.child3 != null + // ? _data.child3[0] != null + // ? `posMaster.orgChild3Id IN (:...child3)` + // : `posMaster.orgChild3Id is null` + // : "1=1", + // { + // child3: _data.child3, + // }, + // ) + // .andWhere( + // _data.child4 != undefined && _data.child4 != null + // ? _data.child4[0] != null + // ? `posMaster.orgChild4Id IN (:...child4)` + // : `posMaster.orgChild4Id is null` + // : "1=1", + // { + // child4: _data.child4, + // }, + // ) .orWhere( new Brackets((qb) => { qb.andWhere( From b11d7e45e2143b8599e92b5ea1f63ace336f0b4c Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 11 Feb 2026 15:45:03 +0700 Subject: [PATCH 071/178] =?UTF-8?q?Fix=20bug=20=E0=B9=81=E0=B8=81=E0=B9=89?= =?UTF-8?q?=E0=B9=84=E0=B8=82=E0=B8=AD=E0=B8=B1=E0=B8=95=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=81=E0=B8=B3=E0=B8=A5=E0=B8=B1=E0=B8=87=20=E0=B9=81?= =?UTF-8?q?=E0=B8=A5=E0=B9=89=E0=B8=A7=E0=B8=A3=E0=B8=B0=E0=B8=9A=E0=B8=9A?= =?UTF-8?q?=E0=B9=81=E0=B8=88=E0=B9=89=E0=B8=87=20error=20#211?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/PositionController.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index d8e9bd7e..eb4ea958 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -1510,7 +1510,8 @@ export class PositionController extends Controller { posMaster.orgRootId !== null && posMaster.orgChild1Id == null && posMaster.orgChild2Id == null && - posMaster.orgChild3Id == null + posMaster.orgChild3Id == null && + posMaster.orgChild4Id == null ) { shortName = posMaster.orgRoot.orgRootShortName; orgId = posMaster.orgRootId; @@ -1518,7 +1519,8 @@ export class PositionController extends Controller { posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && posMaster.orgChild2Id == null && - posMaster.orgChild3Id == null + posMaster.orgChild3Id == null && + posMaster.orgChild4Id == null ) { shortName = posMaster.orgChild1.orgChild1ShortName; orgId = posMaster.orgChild1Id; @@ -1526,7 +1528,8 @@ export class PositionController extends Controller { posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && posMaster.orgChild2Id !== null && - posMaster.orgChild3Id == null + posMaster.orgChild3Id == null && + posMaster.orgChild4Id == null ) { shortName = posMaster.orgChild2.orgChild2ShortName; orgId = posMaster.orgChild2Id; @@ -1534,7 +1537,8 @@ export class PositionController extends Controller { posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && posMaster.orgChild2Id !== null && - posMaster.orgChild3Id !== null + posMaster.orgChild3Id !== null && + posMaster.orgChild4Id == null ) { shortName = posMaster.orgChild3.orgChild3ShortName; orgId = posMaster.orgChild3Id; @@ -1542,7 +1546,8 @@ export class PositionController extends Controller { posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && posMaster.orgChild2Id !== null && - posMaster.orgChild3Id !== null + posMaster.orgChild3Id !== null && + posMaster.orgChild4Id !== null ) { shortName = posMaster.orgChild4.orgChild4ShortName; orgId = posMaster.orgChild4Id; From 7694a83d5a3ee2dbdecf59148cc2980a740bda98 Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 11 Feb 2026 16:50:32 +0700 Subject: [PATCH 072/178] fix --- src/controllers/PositionController.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index eb4ea958..4df79e19 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -5281,28 +5281,24 @@ export class PositionController extends Controller { let level: any = resolveNodeLevel(orgDna); const cannotViewRootPosMaster = - (_data.privilege === "ROOT" && level > 0) || (_data.privilege === "PARENT") || (_data.privilege === "BROTHER" && level > 1) || (_data.privilege === "CHILD" && level > 0) || (_data.privilege === "NORMAL" && level != 0); const cannotViewChild1PosMaster = - (_data.privilege === "ROOT" && level > 1) || (_data.privilege === "PARENT" && level > 1) || (_data.privilege === "BROTHER" && level > 2) || (_data.privilege === "CHILD" && level > 1) || (_data.privilege === "NORMAL" && level !== 1); const cannotViewChild2PosMaster = - (_data.privilege === "ROOT" && level > 2) || (_data.privilege === "PARENT" && level > 2) || (_data.privilege === "BROTHER" && level > 3) || (_data.privilege === "CHILD" && level > 2) || (_data.privilege === "NORMAL" && level !== 2); const cannotViewChild3PosMaster = - (_data.privilege === "ROOT" && level > 3) || (_data.privilege === "PARENT" && level > 3) || (_data.privilege === "BROTHER" && level > 4) || (_data.privilege === "CHILD" && level > 3) || From 3c9e3a1bb6702178dec7bd5d1cae8d8d9740d0c2 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 10:38:16 +0700 Subject: [PATCH 073/178] fix: script org move draf to current save posMasterHistory --- src/controllers/OrganizationController.ts | 95 +++++++++++++++++++++-- src/interfaces/OrgMapping.ts | 20 +++++ src/services/PositionService.ts | 66 +++++++++++++++- 3 files changed, 173 insertions(+), 8 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index a4ef7ac6..813e268c 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -55,9 +55,10 @@ import { import { CreatePosMasterHistoryEmployee, CreatePosMasterHistoryOfficer, + SavePosMasterHistoryOfficer, } from "../services/PositionService"; import { orgStructureCache } from "../utils/OrgStructureCache"; -import { OrgIdMapping, AllOrgMappings } from "../interfaces/OrgMapping"; +import { OrgIdMapping, AllOrgMappings, SavePosMasterHistory } from "../interfaces/OrgMapping"; @Route("api/v1/org") @Tags("Organization") @@ -8110,6 +8111,12 @@ export class OrganizationController extends Controller { // Then delete posMaster records await queryRunner.manager.delete(PosMaster, toDeleteIds); + + await Promise.all( + toDelete.map(async (pos) => { + await SavePosMasterHistoryOfficer(queryRunner, pos.ancestorDNA, null, null); + }), + ); } // 2.4 Process draft positions (UPDATE or INSERT) @@ -8176,6 +8183,7 @@ export class OrganizationController extends Controller { current_holderId: draftPos.next_holderId, statusReport: "DONE", }); + toInsert.push(newPosMaster); } } @@ -8231,6 +8239,11 @@ export class OrganizationController extends Controller { console.error("Error moving draft to current:", error); await queryRunner.rollbackTransaction(); throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการย้ายโครงสร้าง"); + } finally { + if (queryRunner.isTransactionActive) { + await queryRunner.rollbackTransaction(); + } + await queryRunner.release(); } } @@ -8612,18 +8625,27 @@ export class OrganizationController extends Controller { ): Promise<{ deleted: number; updated: number; inserted: number }> { // Extract draft and current posMaster IDs const draftPosMasterIds = Array.from(posMasterMapping.keys()); - const currentPosMasterIds = Array.from(posMasterMapping.values()).map(([currentId]) => currentId); + const currentPosMasterIds = Array.from(posMasterMapping.values()).map( + ([currentId]) => currentId, + ); if (draftPosMasterIds.length === 0) { return { deleted: 0, updated: 0, inserted: 0 }; } + // Fetch draft PosMasters with relations for history tracking + const draftPosMasters = await queryRunner.manager.find(PosMaster, { + where: { id: In(draftPosMasterIds) }, + relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4", "next_holder"], + }); + // Fetch ALL positions for ALL posMasters in just 2 queries const [allDraftPositions, allCurrentPositions] = await Promise.all([ queryRunner.manager.find(Position, { where: { posMasterId: In(draftPosMasterIds), }, + relations: ["posType", "posLevel", "posExecutive"], order: { orderNo: "ASC" }, }), queryRunner.manager.find(Position, { @@ -8656,6 +8678,16 @@ export class OrganizationController extends Controller { const allToInsert: Array = []; const profileUpdates: Map = new Map(); + // Create a map for quick lookup of draft PosMasters with relations + const draftPosMasterMap = new Map(draftPosMasters.map((pm: PosMaster) => [pm.id, pm])); + + // Collect PosMasterHistory calls for selected positions + const historyCalls: Array<{ + ancestorDNA: string; + profileId: string | null; + historyData: SavePosMasterHistory; + }> = []; + // Process each posMaster mapping for (const [draftPosMasterId, [currentPosMasterId, nextHolderId]] of posMasterMapping) { const draftPositions = draftPositionsByMaster.get(draftPosMasterId) || []; @@ -8710,6 +8742,10 @@ export class OrganizationController extends Controller { }); } + if (nextHolderId === null) { + await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); + } + // Collect profile update for the selected position if (nextHolderId != null && draftPos.positionIsSelected) { profileUpdates.set(nextHolderId, { @@ -8718,6 +8754,46 @@ export class OrganizationController extends Controller { posLevelId: draftPos.posLevelId, }); } + + // Collect history data for the selected position + if (nextHolderId != null && draftPos.positionIsSelected) { + const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any; + if (draftPosMaster && draftPosMaster.ancestorDNA) { + // Find the selected position from draft positions + const selectedPos = + draftPositions.find((p) => p.positionIsSelected === true) || draftPos; + historyCalls.push({ + ancestorDNA: draftPosMaster.ancestorDNA, + profileId: nextHolderId, + historyData: { + prefix: draftPosMaster.next_holder?.prefix ?? null, + firstName: draftPosMaster.next_holder?.firstName ?? null, + lastName: draftPosMaster.next_holder?.lastName ?? null, + position: selectedPos.positionName ?? null, + posType: (selectedPos as any).posType?.posTypeName ?? null, + posLevel: (selectedPos as any).posLevel?.posLevelName ?? null, + posExecutive: (selectedPos as any).posExecutive?.posExecutiveName ?? null, + profileId: nextHolderId, + rootDnaId: draftPosMaster.orgRoot?.ancestorDNA ?? null, + child1DnaId: draftPosMaster.orgChild1?.ancestorDNA ?? null, + child2DnaId: draftPosMaster.orgChild2?.ancestorDNA ?? null, + child3DnaId: draftPosMaster.orgChild3?.ancestorDNA ?? null, + child4DnaId: draftPosMaster.orgChild4?.ancestorDNA ?? null, + shortName: + [ + draftPosMaster.orgChild4?.orgChild4ShortName, + draftPosMaster.orgChild3?.orgChild3ShortName, + draftPosMaster.orgChild2?.orgChild2ShortName, + draftPosMaster.orgChild1?.orgChild1ShortName, + draftPosMaster.orgRoot?.orgRootShortName, + ].find((s) => typeof s === "string" && s.trim().length > 0) ?? null, + posMasterNoPrefix: draftPosMaster.posMasterNoPrefix ?? null, + posMasterNo: draftPosMaster.posMasterNo ?? null, + posMasterNoSuffix: draftPosMaster.posMasterNoSuffix ?? null, + }, + }); + } + } } } @@ -8738,7 +8814,7 @@ export class OrganizationController extends Controller { for (let i = 0; i < allToUpdate.length; i += batchSize) { const batch = allToUpdate.slice(i, i + batchSize); await Promise.all( - batch.map(({ id, data }) => queryRunner.manager.update(Position, id, data)) + batch.map(({ id, data }) => queryRunner.manager.update(Position, id, data)), ); } updatedCount = allToUpdate.length; @@ -8759,8 +8835,17 @@ export class OrganizationController extends Controller { const profileUpdateEntries = Array.from(profileUpdates.entries()); await Promise.all( profileUpdateEntries.map(([profileId, data]) => - queryRunner.manager.update(Profile, profileId, data) - ) + queryRunner.manager.update(Profile, profileId, data), + ), + ); + } + + // Save PosMasterHistory for updated positions + if (historyCalls.length > 0) { + await Promise.all( + historyCalls.map(({ ancestorDNA, profileId, historyData }) => + SavePosMasterHistoryOfficer(queryRunner, ancestorDNA, profileId, historyData), + ), ); } diff --git a/src/interfaces/OrgMapping.ts b/src/interfaces/OrgMapping.ts index 1cdea98f..b0eb21bf 100644 --- a/src/interfaces/OrgMapping.ts +++ b/src/interfaces/OrgMapping.ts @@ -22,3 +22,23 @@ export interface AllOrgMappings { orgChild3: OrgIdMapping; orgChild4: OrgIdMapping; } + +export interface SavePosMasterHistory { + prefix: string | null; + firstName: string | null; + lastName: string | null; + position: string | null; + posType: string | null; + posLevel: string | null; + posExecutive: string | null; + profileId: string | null; + rootDnaId: string | null; + child1DnaId: string | null; + child2DnaId: string | null; + child3DnaId: string | null; + child4DnaId: string | null; + shortName: string | null; + posMasterNoPrefix: string | null; + posMasterNo: string | null; + posMasterNoSuffix: string | null; +} diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 67cbecef..34631666 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -1,3 +1,4 @@ +import { SavePosMasterHistory } from "./../interfaces/OrgMapping"; import { AppDataSource } from "../database/data-source"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster"; @@ -44,9 +45,9 @@ export async function CreatePosMasterHistoryOfficer( where: { id: pm.orgRevisionId, orgRevisionIsCurrent: true, - orgRevisionIsDraft: false - } - }) + orgRevisionIsDraft: false, + }, + }); const _null: any = null; const h = new PosMasterHistory(); const selectedPosition = @@ -260,3 +261,62 @@ export async function getTopDegrees(educations: ProfileEducation[]): Promise { + try { + // Type workaround: entity columns are nullable but types don't reflect it + const _null: any = null; + const repoPosMasterHistory = queryRunner.manager.getRepository(PosMasterHistory); + const pmh = await repoPosMasterHistory.findOne({ + where: { + ancestorDNA: posMasterDnaId, + }, + order: { createdAt: "DESC" }, + }); + + // Check if we need to insert a new history record + const shouldInsert = !pmh && profileId && pm; + const profileChanged = pmh && pmh.profileId !== profileId; + + if (shouldInsert || profileChanged) { + // insert new record + const newPmh = new PosMasterHistory(); + newPmh.ancestorDNA = posMasterDnaId; + newPmh.prefix = pm?.prefix ?? _null; + newPmh.firstName = pm?.firstName ?? _null; + newPmh.lastName = pm?.lastName ?? _null; + newPmh.position = pm?.position ?? _null; + newPmh.posType = pm?.posType ?? _null; + newPmh.posLevel = pm?.posLevel ?? _null; + newPmh.posExecutive = pm?.posExecutive ?? _null; + newPmh.profileId = profileId ?? _null; + newPmh.rootDnaId = pm?.rootDnaId ?? _null; + newPmh.child1DnaId = pm?.child1DnaId ?? _null; + newPmh.child2DnaId = pm?.child2DnaId ?? _null; + newPmh.child3DnaId = pm?.child3DnaId ?? _null; + newPmh.child4DnaId = pm?.child4DnaId ?? _null; + newPmh.shortName = pm?.shortName ?? _null; + newPmh.posMasterNoPrefix = pm?.posMasterNoPrefix ?? _null; + newPmh.posMasterNo = pm?.posMasterNo ?? _null; + newPmh.posMasterNoSuffix = pm?.posMasterNoSuffix ?? _null; + // Add audit fields for data integrity + newPmh.createdUserId = "system"; + newPmh.createdFullName = "system"; + newPmh.lastUpdateUserId = "system"; + newPmh.lastUpdateFullName = "system"; + newPmh.createdAt = new Date(); + newPmh.lastUpdatedAt = new Date(); + await queryRunner.manager.save(PosMasterHistory, newPmh); + return true; + } + return true; + } catch (err) { + console.error("SavePosMasterHistoryOfficer error:", err); + return false; + } +} From 65e3740cc26835c2652261586b52f6fe34890e2a Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 12 Feb 2026 11:47:53 +0700 Subject: [PATCH 074/178] Migration add fields isDeleted #210 --- src/controllers/CommandController.ts | 4 +- src/controllers/ImportDataController.ts | 14 ++-- .../OrganizationDotnetController.ts | 12 +-- .../OrganizationUnauthorizeController.ts | 6 +- src/controllers/PosMasterActController.ts | 1 + src/controllers/ProfileAbilityController.ts | 4 +- .../ProfileAbilityEmployeeController.ts | 4 +- .../ProfileAbilityEmployeeTempController.ts | 4 +- .../ProfileAssessmentsController.ts | 4 +- .../ProfileAssessmentsEmployeeController.ts | 3 +- ...rofileAssessmentsEmployeeTempController.ts | 3 +- .../ProfileAssistanceController.ts | 4 +- .../ProfileAssistanceEmployeeController.ts | 4 +- ...ProfileAssistanceEmployeeTempController.ts | 4 +- .../ProfileCertificateController.ts | 4 +- .../ProfileCertificateEmployeeController.ts | 4 +- ...rofileCertificateEmployeeTempController.ts | 4 +- .../ProfileChangeNameController.ts | 4 +- .../ProfileChangeNameEmployeeController.ts | 4 +- ...ProfileChangeNameEmployeeTempController.ts | 4 +- src/controllers/ProfileChildrenController.ts | 4 +- .../ProfileChildrenEmployeeController.ts | 4 +- .../ProfileChildrenEmployeeTempController.ts | 4 +- src/controllers/ProfileController.ts | 62 +++++++++------- .../ProfileDisciplineController.ts | 6 +- .../ProfileDisciplineEmployeeController.ts | 6 +- ...ProfileDisciplineEmployeeTempController.ts | 6 +- src/controllers/ProfileDutyController.ts | 4 +- .../ProfileDutyEmployeeController.ts | 4 +- .../ProfileDutyEmployeeTempController.ts | 4 +- .../ProfileEducationsController.ts | 4 +- .../ProfileEducationsEmployeeController.ts | 4 +- ...ProfileEducationsEmployeeTempController.ts | 4 +- src/controllers/ProfileEmployeeController.ts | 62 +++++++++------- .../ProfileEmployeeTempController.ts | 20 ++--- src/controllers/ProfileHonorController.ts | 4 +- .../ProfileHonorEmployeeController.ts | 4 +- .../ProfileHonorEmployeeTempController.ts | 4 +- src/controllers/ProfileInsigniaController.ts | 4 +- .../ProfileInsigniaEmployeeController.ts | 4 +- .../ProfileInsigniaEmployeeTempController.ts | 4 +- src/controllers/ProfileLeaveController.ts | 5 +- .../ProfileLeaveEmployeeController.ts | 6 +- .../ProfileLeaveEmployeeTempController.ts | 6 +- src/controllers/ProfileNopaidController.ts | 4 +- .../ProfileNopaidEmployeeController.ts | 4 +- .../ProfileNopaidEmployeeTempController.ts | 4 +- src/controllers/ProfileOtherController.ts | 4 +- .../ProfileOtherEmployeeController.ts | 4 +- .../ProfileOtherEmployeeTempController.ts | 4 +- src/controllers/ReportController.ts | 30 +++++--- src/entities/ProfileAbility.ts | 7 ++ src/entities/ProfileAbilityHistory.ts | 7 ++ src/entities/ProfileAssessment.ts | 7 ++ src/entities/ProfileAssessmentHistory.ts | 7 ++ src/entities/ProfileAssistance.ts | 7 ++ src/entities/ProfileAssistanceHistory.ts | 7 ++ src/entities/ProfileCertificate.ts | 7 ++ src/entities/ProfileCertificateHistory.ts | 7 ++ src/entities/ProfileChangeName.ts | 7 ++ src/entities/ProfileChangeNameHistory.ts | 7 ++ src/entities/ProfileChildren.ts | 7 ++ src/entities/ProfileChildrenHistory.ts | 7 ++ src/entities/ProfileDiscipline.ts | 7 ++ src/entities/ProfileDisciplineHistory.ts | 7 ++ src/entities/ProfileDuty.ts | 7 ++ src/entities/ProfileDutyHistory.ts | 7 ++ src/entities/ProfileEducation.ts | 7 ++ src/entities/ProfileEducationHistory.ts | 7 ++ src/entities/ProfileHonor.ts | 7 ++ src/entities/ProfileHonorHistory.ts | 7 ++ src/entities/ProfileInsignia.ts | 7 ++ src/entities/ProfileInsigniaHistory.ts | 7 ++ src/entities/ProfileLeave.ts | 14 ++++ src/entities/ProfileNopaid.ts | 7 ++ src/entities/ProfileNopaidHistory.ts | 7 ++ src/entities/ProfileOther.ts | 7 ++ src/entities/ProfileOtherHistory.ts | 7 ++ src/entities/ProfileSalary.ts | 7 ++ src/entities/ProfileSalaryBackup.ts | 7 ++ src/entities/ProfileSalaryHistory.ts | 7 ++ .../1770870836370-add_fields_isDeleted.ts | 74 +++++++++++++++++++ 82 files changed, 499 insertions(+), 180 deletions(-) create mode 100644 src/migration/1770870836370-add_fields_isDeleted.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 5c801315..ece19088 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -6679,7 +6679,7 @@ export class CommandController extends Controller { profileEdu.profileId = profile.id; const educationLevel = await this.profileEducationRepo.findOne({ select: ["id", "level", "profileId"], - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { level: "DESC" }, }); profileEdu.level = educationLevel == null ? 1 : educationLevel.level + 1; @@ -6871,7 +6871,7 @@ export class CommandController extends Controller { // Insignia if (_oldInsigniaIds.length > 0) { const _insignias = await this.insigniaRepo.find({ - where: { id: In(_oldInsigniaIds) }, + where: { id: In(_oldInsigniaIds), isDeleted: false }, order: { createdAt: "ASC" }, }); for (const oldInsignia of _insignias) { diff --git a/src/controllers/ImportDataController.ts b/src/controllers/ImportDataController.ts index 99e931ea..1c71f9b0 100644 --- a/src/controllers/ImportDataController.ts +++ b/src/controllers/ImportDataController.ts @@ -2475,8 +2475,8 @@ export class ImportDataController extends Controller { }); const educationLevel = await this.profileEducationRepo.findOne({ - select: ["id", "level", "profileId"], - where: { profileId: _item.id }, + select: ["id", "level", "profileId", "isDeleted"], + where: { profileId: _item.id, isDeleted: false }, order: { level: "DESC" }, }); @@ -2607,8 +2607,8 @@ export class ImportDataController extends Controller { }); const educationLevel = await this.profileEducationRepo.findOne({ - select: ["id", "level", "profileEmployeeId"], - where: { profileEmployeeId: _item.id }, + select: ["id", "level", "profileEmployeeId", "isDeleted"], + where: { profileEmployeeId: _item.id, isDeleted: false }, order: { level: "DESC" }, }); @@ -2740,8 +2740,8 @@ export class ImportDataController extends Controller { }); const educationLevel = await this.profileEducationRepo.findOne({ - select: ["id", "level", "profileEmployeeId"], - where: { profileEmployeeId: _item.id }, + select: ["id", "level", "profileEmployeeId", "isDeleted"], + where: { profileEmployeeId: _item.id, isDeleted: false }, order: { level: "DESC" }, }); @@ -5799,7 +5799,7 @@ export class ImportDataController extends Controller { }, }); const eduLevel = await this.profileEducationRepo.findOne({ - where: { profileId: _item.id }, + where: { profileId: _item.id, isDeleted: false }, order: { startDate: "DESC", }, diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 16566685..8f5dd6ac 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -1359,7 +1359,7 @@ export class OrganizationDotnetController extends Controller { order: { commandDateAffect: "DESC" }, }), this.insigniaRepo.findOne({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { receiveDate: "DESC" }, }), ]); @@ -1542,7 +1542,7 @@ export class OrganizationDotnetController extends Controller { order: { commandDateAffect: "DESC" }, }), this.insigniaRepo.findOne({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { receiveDate: "DESC" }, }), ]); @@ -2379,7 +2379,7 @@ export class OrganizationDotnetController extends Controller { order: { commandDateAffect: "DESC" }, }), this.insigniaRepo.findOne({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { receiveDate: "DESC" }, }), ]); @@ -2694,7 +2694,7 @@ export class OrganizationDotnetController extends Controller { order: { commandDateAffect: "DESC" }, }), this.insigniaRepo.findOne({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { receiveDate: "DESC" }, }), ]); @@ -7441,7 +7441,7 @@ export class OrganizationDotnetController extends Controller { : []; const profileEducations = await this.educationRepo.find({ - where: { profileEmployeeId: profile!.id }, + where: { profileEmployeeId: profile!.id, isDeleted: false }, order: { level: "ASC" }, }); _educations = profileEducations.length > 0 @@ -7581,7 +7581,7 @@ export class OrganizationDotnetController extends Controller { : []; const profileEducations = await this.educationRepo.find({ - where: { profileId: profile!.id }, + where: { profileId: profile!.id, isDeleted: false }, order: { level: "ASC" }, }); _educations = profileEducations.length > 0 diff --git a/src/controllers/OrganizationUnauthorizeController.ts b/src/controllers/OrganizationUnauthorizeController.ts index d2ec617d..deaa1f35 100644 --- a/src/controllers/OrganizationUnauthorizeController.ts +++ b/src/controllers/OrganizationUnauthorizeController.ts @@ -423,6 +423,7 @@ export class OrganizationUnauthorizeController extends Controller { year: body.year.toString(), pointSum: MoreThanOrEqual(90), period: body.period.toLocaleUpperCase(), + isDeleted: false } } ); @@ -884,6 +885,7 @@ export class OrganizationUnauthorizeController extends Controller { year: body.year.toString(), pointSum: MoreThanOrEqual(90), period: body.period.toLocaleUpperCase(), + isDeleted: false } } ); @@ -1088,7 +1090,7 @@ export class OrganizationUnauthorizeController extends Controller { order: { commandDateAffect: "DESC" }, }), this.insigniaRepo.findOne({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { receiveDate: "DESC" }, }), ]); @@ -1403,7 +1405,7 @@ export class OrganizationUnauthorizeController extends Controller { order: { commandDateAffect: "DESC" }, }), this.insigniaRepo.findOne({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { receiveDate: "DESC" }, }), ]); diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 7c63ede5..81a61bf4 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -598,6 +598,7 @@ export class PosMasterActController extends Controller { "lastUpdateFullName", "lastUpdatedAt", "dateEnd", + "isDeleted" ], where: { profileId, status: true, isDeleted: false }, }); diff --git a/src/controllers/ProfileAbilityController.ts b/src/controllers/ProfileAbilityController.ts index 0fd2c117..c8012057 100644 --- a/src/controllers/ProfileAbilityController.ts +++ b/src/controllers/ProfileAbilityController.ts @@ -40,7 +40,7 @@ export class ProfileAbilityController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAbilityId = await this.profileAbilityRepo.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAbilityId) { @@ -55,7 +55,7 @@ export class ProfileAbilityController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const getProfileAbilityId = await this.profileAbilityRepo.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAbilityId) { diff --git a/src/controllers/ProfileAbilityEmployeeController.ts b/src/controllers/ProfileAbilityEmployeeController.ts index 3ac38706..d7ba3ae9 100644 --- a/src/controllers/ProfileAbilityEmployeeController.ts +++ b/src/controllers/ProfileAbilityEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileAbilityEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAbilityId = await this.profileAbilityRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAbilityId) { @@ -58,7 +58,7 @@ export class ProfileAbilityEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const getProfileAbilityId = await this.profileAbilityRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAbilityId) { diff --git a/src/controllers/ProfileAbilityEmployeeTempController.ts b/src/controllers/ProfileAbilityEmployeeTempController.ts index 0c0b3723..624c8ded 100644 --- a/src/controllers/ProfileAbilityEmployeeTempController.ts +++ b/src/controllers/ProfileAbilityEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileAbilityEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAbilityId = await this.profileAbilityRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAbilityId) { @@ -57,7 +57,7 @@ export class ProfileAbilityEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const getProfileAbilityId = await this.profileAbilityRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAbilityId) { diff --git a/src/controllers/ProfileAssessmentsController.ts b/src/controllers/ProfileAssessmentsController.ts index f8ca30e0..53906cb2 100644 --- a/src/controllers/ProfileAssessmentsController.ts +++ b/src/controllers/ProfileAssessmentsController.ts @@ -41,7 +41,7 @@ export class ProfileAssessmentsController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAssessments = await this.profileAssessmentsRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssessments) { @@ -59,7 +59,7 @@ export class ProfileAssessmentsController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const getProfileAssessments = await this.profileAssessmentsRepository.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssessments) { diff --git a/src/controllers/ProfileAssessmentsEmployeeController.ts b/src/controllers/ProfileAssessmentsEmployeeController.ts index 26127f3e..0f7f3d81 100644 --- a/src/controllers/ProfileAssessmentsEmployeeController.ts +++ b/src/controllers/ProfileAssessmentsEmployeeController.ts @@ -41,7 +41,7 @@ export class ProfileAssessmentsEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAssessments = await this.profileAssessmentsRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssessments) { @@ -61,6 +61,7 @@ export class ProfileAssessmentsEmployeeController extends Controller { const getProfileAssessments = await this.profileAssessmentsRepository.find({ where: { profileEmployeeId: profileEmployeeId, + isDeleted: false }, order: { createdAt: "ASC" }, }); diff --git a/src/controllers/ProfileAssessmentsEmployeeTempController.ts b/src/controllers/ProfileAssessmentsEmployeeTempController.ts index 2aa62ff7..b6bc45e6 100644 --- a/src/controllers/ProfileAssessmentsEmployeeTempController.ts +++ b/src/controllers/ProfileAssessmentsEmployeeTempController.ts @@ -41,7 +41,7 @@ export class ProfileAssessmentsEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAssessments = await this.profileAssessmentsRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssessments) { @@ -60,6 +60,7 @@ export class ProfileAssessmentsEmployeeTempController extends Controller { const getProfileAssessments = await this.profileAssessmentsRepository.find({ where: { profileEmployeeId: profileEmployeeId, + isDeleted: false }, order: { createdAt: "ASC" }, }); diff --git a/src/controllers/ProfileAssistanceController.ts b/src/controllers/ProfileAssistanceController.ts index abff2a9f..3690f5e6 100644 --- a/src/controllers/ProfileAssistanceController.ts +++ b/src/controllers/ProfileAssistanceController.ts @@ -40,7 +40,7 @@ export class ProfileAssistanceController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAssistanceId = await this.profileAssistanceRepo.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssistanceId) { @@ -55,7 +55,7 @@ export class ProfileAssistanceController extends Controller { // if (_workflow == false) // await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const getProfileAssistanceId = await this.profileAssistanceRepo.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssistanceId) { diff --git a/src/controllers/ProfileAssistanceEmployeeController.ts b/src/controllers/ProfileAssistanceEmployeeController.ts index cbd816b9..88617322 100644 --- a/src/controllers/ProfileAssistanceEmployeeController.ts +++ b/src/controllers/ProfileAssistanceEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileAssistanceEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAssistanceId = await this.profileAssistanceRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssistanceId) { @@ -58,7 +58,7 @@ export class ProfileAssistanceEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const getProfileAssistanceId = await this.profileAssistanceRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssistanceId) { diff --git a/src/controllers/ProfileAssistanceEmployeeTempController.ts b/src/controllers/ProfileAssistanceEmployeeTempController.ts index 534dafa2..da37c3a5 100644 --- a/src/controllers/ProfileAssistanceEmployeeTempController.ts +++ b/src/controllers/ProfileAssistanceEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileAssistanceEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileAssistanceId = await this.profileAssistanceRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssistanceId) { @@ -57,7 +57,7 @@ export class ProfileAssistanceEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const getProfileAssistanceId = await this.profileAssistanceRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); if (!getProfileAssistanceId) { diff --git a/src/controllers/ProfileCertificateController.ts b/src/controllers/ProfileCertificateController.ts index 88d5c492..5cbc019a 100644 --- a/src/controllers/ProfileCertificateController.ts +++ b/src/controllers/ProfileCertificateController.ts @@ -40,7 +40,7 @@ export class ProfileCertificateController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.certificateRepo.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -52,7 +52,7 @@ export class ProfileCertificateController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const record = await this.certificateRepo.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileCertificateEmployeeController.ts b/src/controllers/ProfileCertificateEmployeeController.ts index fb6474e0..6bb4c1df 100644 --- a/src/controllers/ProfileCertificateEmployeeController.ts +++ b/src/controllers/ProfileCertificateEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileCertificateEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.certificateRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -52,7 +52,7 @@ export class ProfileCertificateEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const record = await this.certificateRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileCertificateEmployeeTempController.ts b/src/controllers/ProfileCertificateEmployeeTempController.ts index 20ec902b..f5619023 100644 --- a/src/controllers/ProfileCertificateEmployeeTempController.ts +++ b/src/controllers/ProfileCertificateEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileCertificateEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.certificateRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -51,7 +51,7 @@ export class ProfileCertificateEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const record = await this.certificateRepo.find({ - where: { profileEmployeeId }, + where: { profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 92ce5350..5b772f12 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -41,7 +41,7 @@ export class ProfileChangeNameController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.changeNameRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -53,7 +53,7 @@ export class ProfileChangeNameController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const lists = await this.changeNameRepository.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileChangeNameEmployeeController.ts b/src/controllers/ProfileChangeNameEmployeeController.ts index 51ff0b8e..547368e1 100644 --- a/src/controllers/ProfileChangeNameEmployeeController.ts +++ b/src/controllers/ProfileChangeNameEmployeeController.ts @@ -41,7 +41,7 @@ export class ProfileChangeNameEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.changeNameRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -53,7 +53,7 @@ export class ProfileChangeNameEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const lists = await this.changeNameRepository.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileChangeNameEmployeeTempController.ts b/src/controllers/ProfileChangeNameEmployeeTempController.ts index 152279d6..f7a141f7 100644 --- a/src/controllers/ProfileChangeNameEmployeeTempController.ts +++ b/src/controllers/ProfileChangeNameEmployeeTempController.ts @@ -41,7 +41,7 @@ export class ProfileChangeNameEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.changeNameRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -52,7 +52,7 @@ export class ProfileChangeNameEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const lists = await this.changeNameRepository.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileChildrenController.ts b/src/controllers/ProfileChildrenController.ts index 27075758..577b6781 100644 --- a/src/controllers/ProfileChildrenController.ts +++ b/src/controllers/ProfileChildrenController.ts @@ -41,7 +41,7 @@ export class ProfileChildrenController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.childrenRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -53,7 +53,7 @@ export class ProfileChildrenController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const lists = await this.childrenRepository.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileChildrenEmployeeController.ts b/src/controllers/ProfileChildrenEmployeeController.ts index a8792e7b..7a0cd772 100644 --- a/src/controllers/ProfileChildrenEmployeeController.ts +++ b/src/controllers/ProfileChildrenEmployeeController.ts @@ -41,7 +41,7 @@ export class ProfileChildrenEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.childrenRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -53,7 +53,7 @@ export class ProfileChildrenEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const lists = await this.childrenRepository.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileChildrenEmployeeTempController.ts b/src/controllers/ProfileChildrenEmployeeTempController.ts index 4ee31e5f..2134e954 100644 --- a/src/controllers/ProfileChildrenEmployeeTempController.ts +++ b/src/controllers/ProfileChildrenEmployeeTempController.ts @@ -41,7 +41,7 @@ export class ProfileChildrenEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.childrenRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -52,7 +52,7 @@ export class ProfileChildrenEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const lists = await this.childrenRepository.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 41fe95e7..14a77d7f 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -322,8 +322,8 @@ export class ProfileController extends Controller { ]; const educations = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute"], - where: { profileId: id }, + select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { level: "ASC" }, }); const Education = @@ -575,8 +575,8 @@ export class ProfileController extends Controller { let _child4 = child4?.orgChild4Name; const cert_raw = await this.certificateRepository.find({ - where: { profileId: id }, - select: ["certificateType", "issuer", "certificateNo", "issueDate"], + select: ["certificateType", "issuer", "certificateNo", "issueDate", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const certs = @@ -596,8 +596,8 @@ export class ProfileController extends Controller { }, ]; const training_raw = await this.trainingRepository.find({ - select: ["startDate", "endDate", "place", "department", "name"], - where: { profileId: id }, + select: ["startDate", "endDate", "place", "department", "name", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const trainings = @@ -633,8 +633,8 @@ export class ProfileController extends Controller { ]; const discipline_raw = await this.disciplineRepository.find({ - select: ["refCommandDate", "refCommandNo", "detail"], - where: { profileId: id }, + select: ["refCommandDate", "refCommandNo", "detail", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const disciplines = @@ -655,8 +655,8 @@ export class ProfileController extends Controller { ]; const education_raw = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute"], - where: { profileId: id }, + select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + where: { profileId: id, isDeleted: false }, // order: { lastUpdatedAt: "DESC" }, order: { level: "ASC" }, }); @@ -756,7 +756,7 @@ export class ProfileController extends Controller { insigniaType: true, }, }, - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, order: { receiveDate: "ASC" }, }); const insignias = @@ -796,7 +796,7 @@ export class ProfileController extends Controller { const leave_raw = await this.profileLeaveRepository.find({ relations: { leaveType: true }, - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, order: { dateLeaveStart: "ASC" }, }); const leaves = @@ -1052,8 +1052,8 @@ export class ProfileController extends Controller { let _child4 = child4?.orgChild4Name; const cert_raw = await this.certificateRepository.find({ - where: { profileId: id }, - select: ["certificateType", "issuer", "certificateNo", "issueDate", "expireDate"], + select: ["certificateType", "issuer", "certificateNo", "issueDate", "expireDate", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const certs = @@ -1087,8 +1087,8 @@ export class ProfileController extends Controller { }, ]; const training_raw = await this.trainingRepository.find({ - select: ["place", "department", "name", "duration"], - where: { profileId: id }, + select: ["place", "department", "name", "duration", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const trainings = @@ -1109,8 +1109,8 @@ export class ProfileController extends Controller { ]; const discipline_raw = await this.disciplineRepository.find({ - select: ["refCommandDate", "refCommandNo", "detail", "level"], - where: { profileId: id }, + select: ["refCommandDate", "refCommandNo", "detail", "level", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const disciplines = @@ -1135,6 +1135,7 @@ export class ProfileController extends Controller { const education_raw = await this.profileEducationRepo .createQueryBuilder("education") .where("education.profileId = :profileId", { profileId: id }) + .andWhere("education.isDeleted = :isDeleted", { isDeleted: false }) .orderBy("CASE WHEN education.isEducation = true THEN 1 ELSE 2 END", "ASC") .addOrderBy("education.level", "ASC") .getMany(); @@ -1245,13 +1246,14 @@ export class ProfileController extends Controller { "page", "refCommandDate", "note", + "isDeleted" ], relations: { insignia: { insigniaType: true, }, }, - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, order: { receiveDate: "ASC" }, }); const insignias = @@ -1294,6 +1296,7 @@ export class ProfileController extends Controller { .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") .select([ + "profileLeave.isDeleted", "profileLeave.leaveTypeId", "leaveType.name as name", "leaveType.code as code", @@ -1303,6 +1306,7 @@ export class ProfileController extends Controller { ]) .addSelect("SUM(profileLeave.leaveDays)", "totalLeaveDays") .where("profileLeave.profileId = :profileId", { profileId: id }) + .andWhere("profileLeave.isDeleted = :isDeleted", { isDeleted: false }) .andWhere("profileLeave.status = :status", { status: "approve" }) .groupBy("profileLeave.leaveTypeId") .orderBy("code", "ASC") @@ -1354,6 +1358,7 @@ export class ProfileController extends Controller { .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") .select([ + "profileLeave.isDeleted AS isDeleted", "profileLeave.dateLeaveStart AS dateLeaveStart", "profileLeave.dateLeaveEnd AS dateLeaveEnd", "profileLeave.leaveDays AS leaveDays", @@ -1361,6 +1366,7 @@ export class ProfileController extends Controller { "leaveType.name as name", ]) .where("profileLeave.profileId = :profileId", { profileId: id }) + .andWhere("profileLeave.isDeleted = :isDeleted", { isDeleted: false }) .andWhere("leaveType.code IN (:...codes)", { codes: ["LV-008", "LV-009", "LV-010"] }) .andWhere("profileLeave.status = :status", { status: "approve" }) .orderBy("leaveType.code", "ASC") @@ -1387,7 +1393,7 @@ export class ProfileController extends Controller { }, ]; const children_raw = await this.profileChildrenRepository.find({ - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, }); const children = children_raw.length > 0 @@ -1410,7 +1416,7 @@ export class ProfileController extends Controller { }, ]; const changeName_raw = await this.changeNameRepository.find({ - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const changeName = @@ -1504,13 +1510,13 @@ export class ProfileController extends Controller { ]; const actposition_raw = await this.profileActpositionRepo.find({ - select: ["dateStart", "dateEnd", "position"], + select: ["dateStart", "dateEnd", "position", "isDeleted"], where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const assistance_raw = await this.profileAssistanceRepository.find({ - select: ["dateStart", "dateEnd", "commandName", "agency", "document"], - where: { profileId: id }, + select: ["dateStart", "dateEnd", "commandName", "agency", "document", "isDeleted"], + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); @@ -1566,7 +1572,7 @@ export class ProfileController extends Controller { ]; const actposition = [..._actposition, ..._assistance]; const duty_raw = await this.dutyRepository.find({ - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const duty = @@ -1593,7 +1599,7 @@ export class ProfileController extends Controller { }, ]; const assessments_raw = await this.profileAssessmentsRepository.find({ - where: { profileId: id }, + where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const assessments = @@ -9093,7 +9099,7 @@ export class ProfileController extends Controller { )?.posMasterNo; const latestProfileEducation = await this.profileEducationRepo.findOne({ - where: { profileId: item.id }, + where: { profileId: item.id, isDeleted: false }, order: { level: "ASC" }, }); @@ -11106,7 +11112,7 @@ export class ProfileController extends Controller { } const latestProfileEducation = await this.profileEducationRepo.findOne({ - where: { profileId: item.id }, + where: { profileId: item.id, isDeleted: false }, order: { level: "ASC" }, }); diff --git a/src/controllers/ProfileDisciplineController.ts b/src/controllers/ProfileDisciplineController.ts index 9edfd89d..e7ec5a94 100644 --- a/src/controllers/ProfileDisciplineController.ts +++ b/src/controllers/ProfileDisciplineController.ts @@ -40,7 +40,7 @@ export class ProfileDisciplineController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.disciplineRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -52,7 +52,7 @@ export class ProfileDisciplineController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const lists = await this.disciplineRepository.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -63,7 +63,7 @@ export class ProfileDisciplineController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_SALARY_OFFICER"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_SALARY_OFFICER"); const lists = await this.disciplineRepository.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileDisciplineEmployeeController.ts b/src/controllers/ProfileDisciplineEmployeeController.ts index efb3842b..22dd3f24 100644 --- a/src/controllers/ProfileDisciplineEmployeeController.ts +++ b/src/controllers/ProfileDisciplineEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileDisciplineEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.disciplineRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -52,7 +52,7 @@ export class ProfileDisciplineEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileId); const lists = await this.disciplineRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -63,7 +63,7 @@ export class ProfileDisciplineEmployeeController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_WAGE"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_WAGE"); const lists = await this.disciplineRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileDisciplineEmployeeTempController.ts b/src/controllers/ProfileDisciplineEmployeeTempController.ts index b408dfe1..b1100ab2 100644 --- a/src/controllers/ProfileDisciplineEmployeeTempController.ts +++ b/src/controllers/ProfileDisciplineEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileDisciplineEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.disciplineRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -51,7 +51,7 @@ export class ProfileDisciplineEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const lists = await this.disciplineRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -62,7 +62,7 @@ export class ProfileDisciplineEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_WAGE"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_WAGE"); const lists = await this.disciplineRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileDutyController.ts b/src/controllers/ProfileDutyController.ts index 36aee9ea..43161383 100644 --- a/src/controllers/ProfileDutyController.ts +++ b/src/controllers/ProfileDutyController.ts @@ -36,7 +36,7 @@ export class ProfileDutyController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.dutyRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -48,7 +48,7 @@ export class ProfileDutyController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const lists = await this.dutyRepository.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileDutyEmployeeController.ts b/src/controllers/ProfileDutyEmployeeController.ts index 811ce550..82a3e9b4 100644 --- a/src/controllers/ProfileDutyEmployeeController.ts +++ b/src/controllers/ProfileDutyEmployeeController.ts @@ -36,7 +36,7 @@ export class ProfileDutyEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.dutyRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -48,7 +48,7 @@ export class ProfileDutyEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileId); const lists = await this.dutyRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileDutyEmployeeTempController.ts b/src/controllers/ProfileDutyEmployeeTempController.ts index 31e83d27..09f8f843 100644 --- a/src/controllers/ProfileDutyEmployeeTempController.ts +++ b/src/controllers/ProfileDutyEmployeeTempController.ts @@ -36,7 +36,7 @@ export class ProfileDutyEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.dutyRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -47,7 +47,7 @@ export class ProfileDutyEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const lists = await this.dutyRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileEducationsController.ts b/src/controllers/ProfileEducationsController.ts index 43843960..e9013eea 100644 --- a/src/controllers/ProfileEducationsController.ts +++ b/src/controllers/ProfileEducationsController.ts @@ -42,7 +42,7 @@ export class ProfileEducationsController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileEducation = await this.profileEducationRepo.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { level: "ASC" }, }); if (!getProfileEducation) { @@ -57,7 +57,7 @@ export class ProfileEducationsController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const getProfileEducation = await this.profileEducationRepo.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { level: "ASC" }, }); if (!getProfileEducation) { diff --git a/src/controllers/ProfileEducationsEmployeeController.ts b/src/controllers/ProfileEducationsEmployeeController.ts index cde8c6ec..c81a7309 100644 --- a/src/controllers/ProfileEducationsEmployeeController.ts +++ b/src/controllers/ProfileEducationsEmployeeController.ts @@ -42,7 +42,7 @@ export class ProfileEducationsEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileEducation = await this.profileEducationRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { level: "ASC" }, }); if (!getProfileEducation) { @@ -60,7 +60,7 @@ export class ProfileEducationsEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const getProfileEducation = await this.profileEducationRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { level: "ASC" }, }); if (!getProfileEducation) { diff --git a/src/controllers/ProfileEducationsEmployeeTempController.ts b/src/controllers/ProfileEducationsEmployeeTempController.ts index e90dfc1c..a63c88f7 100644 --- a/src/controllers/ProfileEducationsEmployeeTempController.ts +++ b/src/controllers/ProfileEducationsEmployeeTempController.ts @@ -42,7 +42,7 @@ export class ProfileEducationsEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const getProfileEducation = await this.profileEducationRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { level: "ASC" }, }); if (!getProfileEducation) { @@ -59,7 +59,7 @@ export class ProfileEducationsEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const getProfileEducation = await this.profileEducationRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { level: "ASC" }, }); if (!getProfileEducation) { diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index e87adcac..6b2e6688 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -317,8 +317,8 @@ export class ProfileEmployeeController extends Controller { ]; const educations = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute"], - where: { profileEmployeeId: id }, + select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { level: "ASC" }, }); const Education = @@ -571,8 +571,8 @@ export class ProfileEmployeeController extends Controller { let _child4 = child4?.orgChild4Name; const cert_raw = await this.certificateRepository.find({ - where: { profileEmployeeId: id }, - select: ["certificateType", "issuer", "certificateNo", "issueDate"], + select: ["certificateType", "issuer", "certificateNo", "issueDate", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const certs = @@ -592,8 +592,8 @@ export class ProfileEmployeeController extends Controller { }, ]; const training_raw = await this.trainingRepository.find({ - select: ["startDate", "endDate", "place", "department"], - where: { profileEmployeeId: id }, + select: ["startDate", "endDate", "place", "department", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const trainings = @@ -629,8 +629,8 @@ export class ProfileEmployeeController extends Controller { ]; const discipline_raw = await this.disciplineRepository.find({ - select: ["refCommandDate", "refCommandNo", "detail"], - where: { profileEmployeeId: id }, + select: ["refCommandDate", "refCommandNo", "detail", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const disciplines = @@ -651,8 +651,8 @@ export class ProfileEmployeeController extends Controller { ]; const education_raw = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute"], - where: { profileEmployeeId: id }, + select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, // order: { lastUpdatedAt: "DESC" }, order: { level: "ASC" }, }); @@ -752,7 +752,7 @@ export class ProfileEmployeeController extends Controller { insigniaType: true, }, }, - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { receiveDate: "ASC" }, }); const insignias = @@ -792,7 +792,7 @@ export class ProfileEmployeeController extends Controller { const leave_raw = await this.profileLeaveRepository.find({ relations: { leaveType: true }, - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { dateLeaveStart: "ASC" }, }); const leaves = @@ -1048,8 +1048,8 @@ export class ProfileEmployeeController extends Controller { let _child4 = child4?.orgChild4Name; const cert_raw = await this.certificateRepository.find({ - where: { profileEmployeeId: id }, - select: ["certificateType", "issuer", "certificateNo", "issueDate", "expireDate"], + select: ["certificateType", "issuer", "certificateNo", "issueDate", "expireDate", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const certs = @@ -1083,8 +1083,8 @@ export class ProfileEmployeeController extends Controller { }, ]; const training_raw = await this.trainingRepository.find({ - select: ["place", "department", "name", "duration"], - where: { profileEmployeeId: id }, + select: ["place", "department", "name", "duration", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const trainings = @@ -1105,8 +1105,8 @@ export class ProfileEmployeeController extends Controller { ]; const discipline_raw = await this.disciplineRepository.find({ - select: ["refCommandDate", "refCommandNo", "detail", "level"], - where: { profileEmployeeId: id }, + select: ["refCommandDate", "refCommandNo", "detail", "level", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const disciplines = @@ -1131,6 +1131,7 @@ export class ProfileEmployeeController extends Controller { const education_raw = await this.profileEducationRepo .createQueryBuilder("education") .where("education.profileEmployeeId = :profileId", { profileId: id }) + .andWhere("education.isDeleted = :isDeleted", { isDeleted: false }) .orderBy("CASE WHEN education.isEducation = true THEN 1 ELSE 2 END", "ASC") .addOrderBy("education.level", "ASC") .getMany(); @@ -1241,13 +1242,14 @@ export class ProfileEmployeeController extends Controller { "page", "refCommandDate", "note", + "isDeleted" ], relations: { insignia: { insigniaType: true, }, }, - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { receiveDate: "ASC" }, }); const insignias = @@ -1290,6 +1292,7 @@ export class ProfileEmployeeController extends Controller { .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") .select([ + "profileLeave.isDeleted", "profileLeave.leaveTypeId", "leaveType.name as name", "leaveType.code as code", @@ -1299,6 +1302,7 @@ export class ProfileEmployeeController extends Controller { ]) .addSelect("SUM(profileLeave.leaveDays)", "totalLeaveDays") .where("profileLeave.profileEmployeeId = :profileId", { profileId: id }) + .andWhere("profileLeave.isDeleted = :isDeleted", { isDeleted: false }) .andWhere("profileLeave.status = :status", { status: "approve" }) .groupBy("profileLeave.leaveTypeId") .orderBy("code", "ASC") @@ -1350,6 +1354,7 @@ export class ProfileEmployeeController extends Controller { .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") .select([ + "profileLeave.isDeleted AS isDeleted", "profileLeave.dateLeaveStart AS dateLeaveStart", "profileLeave.dateLeaveEnd AS dateLeaveEnd", "profileLeave.leaveDays AS leaveDays", @@ -1357,6 +1362,7 @@ export class ProfileEmployeeController extends Controller { "leaveType.name as name", ]) .where("profileLeave.profileEmployeeId = :profileId", { profileId: id }) + .andWhere("profileLeave.isDeleted = :isDeleted", { isDeleted: false }) .andWhere("leaveType.code IN (:...codes)", { codes: ["LV-008", "LV-009", "LV-010"] }) .andWhere("profileLeave.status = :status", { status: "approve" }) .orderBy("leaveType.code", "ASC") @@ -1383,7 +1389,7 @@ export class ProfileEmployeeController extends Controller { }, ]; const children_raw = await this.profileChildrenRepository.find({ - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, }); const children = children_raw.length > 0 @@ -1406,7 +1412,7 @@ export class ProfileEmployeeController extends Controller { }, ]; const changeName_raw = await this.changeNameRepository.find({ - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const changeName = @@ -1500,13 +1506,13 @@ export class ProfileEmployeeController extends Controller { ]; const actposition_raw = await this.profileActpositionRepo.find({ - select: ["dateStart", "dateEnd", "position"], + select: ["dateStart", "dateEnd", "position", "isDeleted"], where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const assistance_raw = await this.profileAssistanceRepository.find({ - select: ["dateStart", "dateEnd", "commandName", "agency", "document"], - where: { profileEmployeeId: id }, + select: ["dateStart", "dateEnd", "commandName", "agency", "document", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); @@ -1562,7 +1568,7 @@ export class ProfileEmployeeController extends Controller { ]; const actposition = [..._actposition, ..._assistance]; const duty_raw = await this.dutyRepository.find({ - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const duty = @@ -1589,7 +1595,7 @@ export class ProfileEmployeeController extends Controller { }, ]; const assessments_raw = await this.profileAssessmentsRepository.find({ - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const assessments = @@ -3854,7 +3860,7 @@ export class ProfileEmployeeController extends Controller { )?.posMasterNo; const latestProfileEducation = await this.profileEducationRepo.findOne({ - where: { profileEmployeeId: item.id }, + where: { profileEmployeeId: item.id, isDeleted: false }, order: { level: "ASC" }, }); @@ -5953,7 +5959,7 @@ export class ProfileEmployeeController extends Controller { } const latestProfileEducation = await this.profileEducationRepo.findOne({ - where: { profileEmployeeId: item.id }, + where: { profileEmployeeId: item.id, isDeleted: false }, order: { level: "ASC" }, }); diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index efe59ca2..096b3190 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -289,8 +289,8 @@ export class ProfileEmployeeTempController extends Controller { }, ]; const educations = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute"], - where: { profileEmployeeId: id }, + select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { level: "ASC" }, }); const Education = @@ -547,8 +547,8 @@ export class ProfileEmployeeTempController extends Controller { let _child4 = child4?.orgChild4Name; const cert_raw = await this.certificateRepository.find({ - where: { profileEmployeeId: id }, - select: ["certificateType", "issuer", "certificateNo", "issueDate"], + select: ["certificateType", "issuer", "certificateNo", "issueDate", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const certs = @@ -627,8 +627,8 @@ export class ProfileEmployeeTempController extends Controller { ]; const education_raw = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute"], - where: { profileEmployeeId: id }, + select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + where: { profileEmployeeId: id, isDeleted: false }, // order: { lastUpdatedAt: "DESC" }, order: { level: "ASC" }, }); @@ -734,7 +734,7 @@ export class ProfileEmployeeTempController extends Controller { insigniaType: true, }, }, - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { receiveDate: "ASC" }, }); const insignias = @@ -774,7 +774,7 @@ export class ProfileEmployeeTempController extends Controller { const leave_raw = await this.profileLeaveRepository.find({ relations: { leaveType: true }, - where: { profileEmployeeId: id }, + where: { profileEmployeeId: id, isDeleted: false }, order: { dateLeaveStart: "ASC" }, }); const leaves = @@ -2370,7 +2370,7 @@ export class ProfileEmployeeTempController extends Controller { )?.posMasterNo; const latestProfileEducation = await this.profileEducationRepo.findOne({ - where: { profileEmployeeId: item.id }, + where: { profileEmployeeId: item.id, isDeleted: false }, order: { level: "ASC" }, }); @@ -4043,7 +4043,7 @@ export class ProfileEmployeeTempController extends Controller { )?.posMasterNo; const latestProfileEducation = await this.profileEducationRepo.findOne({ - where: { profileEmployeeId: item.id }, + where: { profileEmployeeId: item.id, isDeleted: false }, order: { level: "ASC" }, }); diff --git a/src/controllers/ProfileHonorController.ts b/src/controllers/ProfileHonorController.ts index 114c76b3..057f6ad0 100644 --- a/src/controllers/ProfileHonorController.ts +++ b/src/controllers/ProfileHonorController.ts @@ -36,7 +36,7 @@ export class ProfileHonorController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.honorRepo.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -48,7 +48,7 @@ export class ProfileHonorController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const record = await this.honorRepo.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileHonorEmployeeController.ts b/src/controllers/ProfileHonorEmployeeController.ts index 11e6ddf8..bacb5ca5 100644 --- a/src/controllers/ProfileHonorEmployeeController.ts +++ b/src/controllers/ProfileHonorEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileHonorEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.honorRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -52,7 +52,7 @@ export class ProfileHonorEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileEmployeeId); const record = await this.honorRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileHonorEmployeeTempController.ts b/src/controllers/ProfileHonorEmployeeTempController.ts index e163aa05..dbf1ceb3 100644 --- a/src/controllers/ProfileHonorEmployeeTempController.ts +++ b/src/controllers/ProfileHonorEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileHonorEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const record = await this.honorRepo.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -51,7 +51,7 @@ export class ProfileHonorEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileEmployeeId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const record = await this.honorRepo.find({ - where: { profileEmployeeId: profileEmployeeId }, + where: { profileEmployeeId: profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileInsigniaController.ts b/src/controllers/ProfileInsigniaController.ts index 47e267f4..af871c69 100644 --- a/src/controllers/ProfileInsigniaController.ts +++ b/src/controllers/ProfileInsigniaController.ts @@ -50,7 +50,7 @@ export class ProfileInsigniaController extends Controller { insigniaType: true, }, }, - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -67,7 +67,7 @@ export class ProfileInsigniaController extends Controller { insigniaType: true, }, }, - where: { profileId }, + where: { profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileInsigniaEmployeeController.ts b/src/controllers/ProfileInsigniaEmployeeController.ts index 1921a5fd..e0d3ad82 100644 --- a/src/controllers/ProfileInsigniaEmployeeController.ts +++ b/src/controllers/ProfileInsigniaEmployeeController.ts @@ -47,7 +47,7 @@ export class ProfileInsigniaEmployeeController extends Controller { insigniaType: true, }, }, - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -64,7 +64,7 @@ export class ProfileInsigniaEmployeeController extends Controller { insigniaType: true, }, }, - where: { profileEmployeeId }, + where: { profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileInsigniaEmployeeTempController.ts b/src/controllers/ProfileInsigniaEmployeeTempController.ts index c0f3ef71..6f11cec3 100644 --- a/src/controllers/ProfileInsigniaEmployeeTempController.ts +++ b/src/controllers/ProfileInsigniaEmployeeTempController.ts @@ -47,7 +47,7 @@ export class ProfileInsigniaEmployeeTempController extends Controller { insigniaType: true, }, }, - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -63,7 +63,7 @@ export class ProfileInsigniaEmployeeTempController extends Controller { insigniaType: true, }, }, - where: { profileEmployeeId }, + where: { profileEmployeeId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileLeaveController.ts b/src/controllers/ProfileLeaveController.ts index f5601c02..a6624bbb 100644 --- a/src/controllers/ProfileLeaveController.ts +++ b/src/controllers/ProfileLeaveController.ts @@ -120,7 +120,7 @@ export class ProfileLeaveController extends Controller { } const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -136,6 +136,7 @@ export class ProfileLeaveController extends Controller { where: { profileId: profileId, // status: Not("cancel") + isDeleted: false }, order: { createdAt: "ASC" }, }); @@ -148,7 +149,7 @@ export class ProfileLeaveController extends Controller { if (_workflow == false) await new permission().PermissionGet(req, "SYS_SALARY_OFFICER"); const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileId }, + where: { profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileLeaveEmployeeController.ts b/src/controllers/ProfileLeaveEmployeeController.ts index feb6891e..ade4a5c4 100644 --- a/src/controllers/ProfileLeaveEmployeeController.ts +++ b/src/controllers/ProfileLeaveEmployeeController.ts @@ -43,7 +43,7 @@ export class ProfileLeaveEmployeeController extends Controller { } const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -56,7 +56,7 @@ export class ProfileLeaveEmployeeController extends Controller { await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileId); const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -68,7 +68,7 @@ export class ProfileLeaveEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionGet(req, "SYS_WAGE"); const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileLeaveEmployeeTempController.ts b/src/controllers/ProfileLeaveEmployeeTempController.ts index 39506d32..85960fa5 100644 --- a/src/controllers/ProfileLeaveEmployeeTempController.ts +++ b/src/controllers/ProfileLeaveEmployeeTempController.ts @@ -43,7 +43,7 @@ export class ProfileLeaveEmployeeTempController extends Controller { } const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -55,7 +55,7 @@ export class ProfileLeaveEmployeeTempController extends Controller { if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); @@ -67,7 +67,7 @@ export class ProfileLeaveEmployeeTempController extends Controller { if (_workflow == false) await new permission().PermissionGet(req, "SYS_WAGE"); const record = await this.leaveRepo.find({ relations: { leaveType: true }, - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(record); diff --git a/src/controllers/ProfileNopaidController.ts b/src/controllers/ProfileNopaidController.ts index 491aaae5..d0760d13 100644 --- a/src/controllers/ProfileNopaidController.ts +++ b/src/controllers/ProfileNopaidController.ts @@ -36,7 +36,7 @@ export class ProfileNopaidController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.nopaidRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -48,7 +48,7 @@ export class ProfileNopaidController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const lists = await this.nopaidRepository.find({ - where: { profileId }, + where: { profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileNopaidEmployeeController.ts b/src/controllers/ProfileNopaidEmployeeController.ts index fd5b7353..e5849e8e 100644 --- a/src/controllers/ProfileNopaidEmployeeController.ts +++ b/src/controllers/ProfileNopaidEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileNopaidEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.nopaidRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -51,7 +51,7 @@ export class ProfileNopaidEmployeeController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_EMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_EMP"); const lists = await this.nopaidRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileNopaidEmployeeTempController.ts b/src/controllers/ProfileNopaidEmployeeTempController.ts index 37dec407..fe6edab3 100644 --- a/src/controllers/ProfileNopaidEmployeeTempController.ts +++ b/src/controllers/ProfileNopaidEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileNopaidEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.nopaidRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -51,7 +51,7 @@ export class ProfileNopaidEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const lists = await this.nopaidRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileOtherController.ts b/src/controllers/ProfileOtherController.ts index 79ae8c8f..430af83e 100644 --- a/src/controllers/ProfileOtherController.ts +++ b/src/controllers/ProfileOtherController.ts @@ -37,7 +37,7 @@ export class ProfileOtherController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.otherRepository.find({ - where: { profileId: profile.id }, + where: { profileId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -49,7 +49,7 @@ export class ProfileOtherController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); const lists = await this.otherRepository.find({ - where: { profileId: profileId }, + where: { profileId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileOtherEmployeeController.ts b/src/controllers/ProfileOtherEmployeeController.ts index 0a6622b6..c8894afe 100644 --- a/src/controllers/ProfileOtherEmployeeController.ts +++ b/src/controllers/ProfileOtherEmployeeController.ts @@ -40,7 +40,7 @@ export class ProfileOtherEmployeeController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.otherRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -52,7 +52,7 @@ export class ProfileOtherEmployeeController extends Controller { if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileId); const lists = await this.otherRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ProfileOtherEmployeeTempController.ts b/src/controllers/ProfileOtherEmployeeTempController.ts index 38962384..c20914fe 100644 --- a/src/controllers/ProfileOtherEmployeeTempController.ts +++ b/src/controllers/ProfileOtherEmployeeTempController.ts @@ -40,7 +40,7 @@ export class ProfileOtherEmployeeTempController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const lists = await this.otherRepository.find({ - where: { profileEmployeeId: profile.id }, + where: { profileEmployeeId: profile.id, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); @@ -51,7 +51,7 @@ export class ProfileOtherEmployeeTempController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_TEMP"); if (_workflow == false) await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP"); const lists = await this.otherRepository.find({ - where: { profileEmployeeId: profileId }, + where: { profileEmployeeId: profileId, isDeleted: false }, order: { createdAt: "ASC" }, }); return new HttpSuccess(lists); diff --git a/src/controllers/ReportController.ts b/src/controllers/ReportController.ts index 2e72fc6b..3966fb21 100644 --- a/src/controllers/ReportController.ts +++ b/src/controllers/ReportController.ts @@ -3659,8 +3659,9 @@ export class ReportController extends Controller { if (profileIds.length > 0) { educationsData = await this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...profileIds)", { profileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -3844,8 +3845,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child1ProfileIds)", { child1ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -4025,8 +4027,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child2ProfileIds)", { child2ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -4206,8 +4209,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child3ProfileIds)", { child3ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -4385,8 +4389,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child4ProfileIds)", { child4ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -8955,8 +8960,9 @@ export class ReportController extends Controller { if (profileIds.length > 0) { educationsData = await this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...profileIds)", { profileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -9146,8 +9152,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child1ProfileIds)", { child1ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -9333,8 +9340,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child2ProfileIds)", { child2ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -9521,8 +9529,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child3ProfileIds)", { child3ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, @@ -9708,8 +9717,9 @@ export class ReportController extends Controller { this.profileEducationRepository .createQueryBuilder("pe") - .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level"]) + .select(["pe.profileId", "pe.finishDate", "pe.degree", "pe.level", "pe.isDeleted"]) .where("pe.profileId IN (:...child4ProfileIds)", { child4ProfileIds }) + .andWhere("pe.isDeleted = :isDeleted", { isDeleted: false }) .andWhere( `(pe.profileId, COALESCE(pe.finishDate, '1900-01-01'), pe.level) IN ( SELECT pe2.profileId, diff --git a/src/entities/ProfileAbility.ts b/src/entities/ProfileAbility.ts index d2ea83d4..dcba7ee1 100644 --- a/src/entities/ProfileAbility.ts +++ b/src/entities/ProfileAbility.ts @@ -82,6 +82,13 @@ export class ProfileAbility extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileAbilityHistory, (profileAbilityHistory) => profileAbilityHistory.histories, diff --git a/src/entities/ProfileAbilityHistory.ts b/src/entities/ProfileAbilityHistory.ts index 16b0de99..01aff27b 100644 --- a/src/entities/ProfileAbilityHistory.ts +++ b/src/entities/ProfileAbilityHistory.ts @@ -66,6 +66,13 @@ export class ProfileAbilityHistory extends EntityBase { }) profileAbilityId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => ProfileAbility, (profileAbility) => profileAbility.profileAbilityHistorys) @JoinColumn({ name: "profileAbilityId" }) histories: ProfileAbility; diff --git a/src/entities/ProfileAssessment.ts b/src/entities/ProfileAssessment.ts index 6ae060ed..f08ad25e 100644 --- a/src/entities/ProfileAssessment.ts +++ b/src/entities/ProfileAssessment.ts @@ -100,6 +100,13 @@ export class ProfileAssessment extends EntityBase { }) pointSumTotal: number; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileAssessmentHistory, (profileAssessmentHistory) => profileAssessmentHistory.histories, diff --git a/src/entities/ProfileAssessmentHistory.ts b/src/entities/ProfileAssessmentHistory.ts index 224ea49c..5bfe8524 100644 --- a/src/entities/ProfileAssessmentHistory.ts +++ b/src/entities/ProfileAssessmentHistory.ts @@ -96,6 +96,13 @@ export class ProfileAssessmentHistory extends EntityBase { }) profileAssessmentId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileAssessment, (profileAssessment) => profileAssessment.profileAssessmentHistorys, diff --git a/src/entities/ProfileAssistance.ts b/src/entities/ProfileAssistance.ts index ad01f585..06cd9ffb 100644 --- a/src/entities/ProfileAssistance.ts +++ b/src/entities/ProfileAssistance.ts @@ -94,6 +94,13 @@ export class ProfileAssistance extends EntityBase { }) commandId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => Command, (command) => command.profileSalarys) @JoinColumn({ name: "commandId" }) command: Command; diff --git a/src/entities/ProfileAssistanceHistory.ts b/src/entities/ProfileAssistanceHistory.ts index 344c6dc0..8a106203 100644 --- a/src/entities/ProfileAssistanceHistory.ts +++ b/src/entities/ProfileAssistanceHistory.ts @@ -62,6 +62,13 @@ export class ProfileAssistanceHistory extends EntityBase { }) profileAssistanceId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileAssistance, (profileAssistance) => profileAssistance.profileAssistanceHistorys, diff --git a/src/entities/ProfileCertificate.ts b/src/entities/ProfileCertificate.ts index 730bde73..867c7797 100644 --- a/src/entities/ProfileCertificate.ts +++ b/src/entities/ProfileCertificate.ts @@ -74,6 +74,13 @@ export class ProfileCertificate extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileCertificateHistory, (profileCertificateHistory) => profileCertificateHistory.histories, diff --git a/src/entities/ProfileCertificateHistory.ts b/src/entities/ProfileCertificateHistory.ts index 022d1e17..0e9a0834 100644 --- a/src/entities/ProfileCertificateHistory.ts +++ b/src/entities/ProfileCertificateHistory.ts @@ -58,6 +58,13 @@ export class ProfileCertificateHistory extends EntityBase { }) profileCertificateId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileCertificate, (profileCertificate) => profileCertificate.profileCertificateHistories, diff --git a/src/entities/ProfileChangeName.ts b/src/entities/ProfileChangeName.ts index 74767fa2..b02117fc 100644 --- a/src/entities/ProfileChangeName.ts +++ b/src/entities/ProfileChangeName.ts @@ -77,6 +77,13 @@ export class ProfileChangeName extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileChangeNameHistory, (profileChangeNameHistory) => profileChangeNameHistory.histories, diff --git a/src/entities/ProfileChangeNameHistory.ts b/src/entities/ProfileChangeNameHistory.ts index 619344dd..5d296161 100644 --- a/src/entities/ProfileChangeNameHistory.ts +++ b/src/entities/ProfileChangeNameHistory.ts @@ -60,6 +60,13 @@ export class ProfileChangeNameHistory extends EntityBase { }) profileChangeNameId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileChangeName, (profileChangeName) => profileChangeName.profileChangeNameHistories, diff --git a/src/entities/ProfileChildren.ts b/src/entities/ProfileChildren.ts index 23f3b507..d4f07c72 100644 --- a/src/entities/ProfileChildren.ts +++ b/src/entities/ProfileChildren.ts @@ -72,6 +72,13 @@ export class ProfileChildren extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => Profile, (Profile) => Profile.profileChildrens) @JoinColumn({ name: "profileId" }) profile: Profile; diff --git a/src/entities/ProfileChildrenHistory.ts b/src/entities/ProfileChildrenHistory.ts index 39e34b74..a402c41e 100644 --- a/src/entities/ProfileChildrenHistory.ts +++ b/src/entities/ProfileChildrenHistory.ts @@ -55,6 +55,13 @@ export class ProfileChildrenHistory extends EntityBase { }) profileChildrenId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + // @ManyToOne(() => ProfileChildren, (profileChildren) => profileChildren.profileChildrenHistories) // @JoinColumn({ name: "profileChildrenId" }) // histories: ProfileChildren; diff --git a/src/entities/ProfileDiscipline.ts b/src/entities/ProfileDiscipline.ts index 6220b712..0065528b 100644 --- a/src/entities/ProfileDiscipline.ts +++ b/src/entities/ProfileDiscipline.ts @@ -82,6 +82,13 @@ export class ProfileDiscipline extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileDisciplineHistory, (profileDisciplineHistory) => profileDisciplineHistory.histories, diff --git a/src/entities/ProfileDisciplineHistory.ts b/src/entities/ProfileDisciplineHistory.ts index f265a825..e7dfee58 100644 --- a/src/entities/ProfileDisciplineHistory.ts +++ b/src/entities/ProfileDisciplineHistory.ts @@ -65,6 +65,13 @@ export class ProfileDisciplineHistory extends EntityBase { }) isUpload: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileDiscipline, (profileDiscipline) => profileDiscipline.profileDisciplineHistories, diff --git a/src/entities/ProfileDuty.ts b/src/entities/ProfileDuty.ts index 5684f650..7c2d8338 100644 --- a/src/entities/ProfileDuty.ts +++ b/src/entities/ProfileDuty.ts @@ -82,6 +82,13 @@ export class ProfileDuty extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany(() => ProfileDutyHistory, (profileDutyHistory) => profileDutyHistory.histories) profileDutyHistories: ProfileDutyHistory[]; diff --git a/src/entities/ProfileDutyHistory.ts b/src/entities/ProfileDutyHistory.ts index e00f4d5f..24c61a5c 100644 --- a/src/entities/ProfileDutyHistory.ts +++ b/src/entities/ProfileDutyHistory.ts @@ -66,6 +66,13 @@ export class ProfileDutyHistory extends EntityBase { }) profileDutyId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => ProfileDuty, (profileDuty) => profileDuty.profileDutyHistories) @JoinColumn({ name: "profileDutyId" }) histories: ProfileDuty; diff --git a/src/entities/ProfileEducation.ts b/src/entities/ProfileEducation.ts index 351aeec0..0b7c6f6e 100644 --- a/src/entities/ProfileEducation.ts +++ b/src/entities/ProfileEducation.ts @@ -196,6 +196,13 @@ export class ProfileEducation extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany( () => ProfileEducationHistory, (profileEducationHistory) => profileEducationHistory.histories, diff --git a/src/entities/ProfileEducationHistory.ts b/src/entities/ProfileEducationHistory.ts index 7a8d5497..6ffb118c 100644 --- a/src/entities/ProfileEducationHistory.ts +++ b/src/entities/ProfileEducationHistory.ts @@ -166,6 +166,13 @@ export class ProfileEducationHistory extends EntityBase { }) profileEducationId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne( () => ProfileEducation, (profileEducation) => profileEducation.profileEducationHistories, diff --git a/src/entities/ProfileHonor.ts b/src/entities/ProfileHonor.ts index 481e195f..73ec072f 100644 --- a/src/entities/ProfileHonor.ts +++ b/src/entities/ProfileHonor.ts @@ -89,6 +89,13 @@ export class ProfileHonor extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany(() => ProfileHonorHistory, (profileHonorHistory) => profileHonorHistory.histories) profileHonorHistories: ProfileHonorHistory[]; diff --git a/src/entities/ProfileHonorHistory.ts b/src/entities/ProfileHonorHistory.ts index 59338365..d22137ab 100644 --- a/src/entities/ProfileHonorHistory.ts +++ b/src/entities/ProfileHonorHistory.ts @@ -73,6 +73,13 @@ export class ProfileHonorHistory extends EntityBase { }) isUpload: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => ProfileHonor, (profileHonor) => profileHonor.profileHonorHistories) @JoinColumn({ name: "profileHonorId" }) histories: ProfileHonor; diff --git a/src/entities/ProfileInsignia.ts b/src/entities/ProfileInsignia.ts index b7fa824d..d09e71ee 100644 --- a/src/entities/ProfileInsignia.ts +++ b/src/entities/ProfileInsignia.ts @@ -137,6 +137,13 @@ export class ProfileInsignia extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => Insignia, (v) => v.profileInsignias) insignia: Insignia; diff --git a/src/entities/ProfileInsigniaHistory.ts b/src/entities/ProfileInsigniaHistory.ts index cc4fa5d7..4381b900 100644 --- a/src/entities/ProfileInsigniaHistory.ts +++ b/src/entities/ProfileInsigniaHistory.ts @@ -65,6 +65,13 @@ export class ProfileInsigniaHistory extends EntityBase { }) profileInsigniaId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => ProfileInsignia, (profileInsignia) => profileInsignia.profileInsigniaHistories) @JoinColumn({ name: "profileInsigniaId" }) histories: ProfileInsignia; diff --git a/src/entities/ProfileLeave.ts b/src/entities/ProfileLeave.ts index 2728b32b..f037e303 100644 --- a/src/entities/ProfileLeave.ts +++ b/src/entities/ProfileLeave.ts @@ -100,6 +100,13 @@ export class ProfileLeave extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany(() => ProfileLeaveHistory, (v) => v.profileLeave) histories: ProfileLeaveHistory[]; @@ -124,6 +131,13 @@ export class ProfileLeaveHistory extends ProfileLeave { }) profileLeaveId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean = false; + @ManyToOne(() => ProfileLeave, (v) => v.histories) profileLeave: ProfileLeave; } diff --git a/src/entities/ProfileNopaid.ts b/src/entities/ProfileNopaid.ts index 1f90025f..7eb2e82f 100644 --- a/src/entities/ProfileNopaid.ts +++ b/src/entities/ProfileNopaid.ts @@ -74,6 +74,13 @@ export class ProfileNopaid extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany(() => ProfileNopaidHistory, (profileNopaidHistory) => profileNopaidHistory.histories) profileNopaidHistories: ProfileNopaidHistory[]; diff --git a/src/entities/ProfileNopaidHistory.ts b/src/entities/ProfileNopaidHistory.ts index 905395ef..6bb14d0b 100644 --- a/src/entities/ProfileNopaidHistory.ts +++ b/src/entities/ProfileNopaidHistory.ts @@ -58,6 +58,13 @@ export class ProfileNopaidHistory extends EntityBase { }) profileNopaidId: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => ProfileNopaid, (profileNopaid) => profileNopaid.profileNopaidHistories) @JoinColumn({ name: "profileNopaidId" }) histories: ProfileNopaid; diff --git a/src/entities/ProfileOther.ts b/src/entities/ProfileOther.ts index b64171ed..497886be 100644 --- a/src/entities/ProfileOther.ts +++ b/src/entities/ProfileOther.ts @@ -44,6 +44,13 @@ export class ProfileOther extends EntityBase { }) isEntry: boolean; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @OneToMany(() => ProfileOtherHistory, (profileOtherHistory) => profileOtherHistory.histories) profileOtherHistories: ProfileOtherHistory[]; diff --git a/src/entities/ProfileOtherHistory.ts b/src/entities/ProfileOtherHistory.ts index 15ea3588..654425a7 100644 --- a/src/entities/ProfileOtherHistory.ts +++ b/src/entities/ProfileOtherHistory.ts @@ -28,6 +28,13 @@ export class ProfileOtherHistory extends EntityBase { }) date: Date; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => ProfileOther, (profileOther) => profileOther.profileOtherHistories) @JoinColumn({ name: "profileOtherId" }) histories: ProfileOther; diff --git a/src/entities/ProfileSalary.ts b/src/entities/ProfileSalary.ts index ddbe184b..1ab7acab 100644 --- a/src/entities/ProfileSalary.ts +++ b/src/entities/ProfileSalary.ts @@ -287,6 +287,13 @@ export class ProfileSalary extends EntityBase { }) positionArea: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => Command, (command) => command.profileSalarys) @JoinColumn({ name: "commandId" }) command: Command; diff --git a/src/entities/ProfileSalaryBackup.ts b/src/entities/ProfileSalaryBackup.ts index 29cdd325..1274e319 100644 --- a/src/entities/ProfileSalaryBackup.ts +++ b/src/entities/ProfileSalaryBackup.ts @@ -292,4 +292,11 @@ export class ProfileSalaryBackup extends EntityBase { }) positionArea: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + } \ No newline at end of file diff --git a/src/entities/ProfileSalaryHistory.ts b/src/entities/ProfileSalaryHistory.ts index ea6af033..b5482c29 100644 --- a/src/entities/ProfileSalaryHistory.ts +++ b/src/entities/ProfileSalaryHistory.ts @@ -228,6 +228,13 @@ export class ProfileSalaryHistory extends EntityBase { }) posNumCodeSitAbb: string; + @Column({ + nullable: false, + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + @ManyToOne(() => Command, (command) => command.profileSalaryHistorys) @JoinColumn({ name: "commandId" }) command: Command; diff --git a/src/migration/1770870836370-add_fields_isDeleted.ts b/src/migration/1770870836370-add_fields_isDeleted.ts new file mode 100644 index 00000000..6d04805c --- /dev/null +++ b/src/migration/1770870836370-add_fields_isDeleted.ts @@ -0,0 +1,74 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddFieldsIsDeleted1770870836370 implements MigrationInterface { + name = 'AddFieldsIsDeleted1770870836370' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileCertificateHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileCertificate\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileInsigniaHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileInsignia\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileHonorHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileHonor\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileAssessmentHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileAssessment\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileLeave\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileDutyHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileDuty\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileNopaidHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileNopaid\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileDisciplineHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileDiscipline\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileChangeNameHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileChangeName\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileEducationHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileEducation\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileAbilityHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileAbility\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileOtherHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileOther\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileChildrenHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileChildren\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileSalaryHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileSalary\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileAssistanceHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileAssistance\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileSalaryBackup\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileSalaryBackup\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileAssistance\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileAssistanceHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileSalary\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileSalaryHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileChildren\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileChildrenHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileOther\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileOtherHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileAbility\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileAbilityHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileEducation\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileEducationHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileChangeName\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileChangeNameHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileDiscipline\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileDisciplineHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileNopaid\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileNopaidHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileDuty\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileDutyHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileLeave\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileAssessment\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileAssessmentHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileHonor\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileHonorHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileInsignia\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileInsigniaHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileCertificate\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileCertificateHistory\` DROP COLUMN \`isDeleted\``); + } + +} From 13fa8cbf2402e9523a146ee7c1429f9da83d37e2 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 11:53:24 +0700 Subject: [PATCH 075/178] fix: script case nextholder id null --- src/controllers/OrganizationController.ts | 121 +++------------------- 1 file changed, 13 insertions(+), 108 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 813e268c..b0a007f5 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8167,6 +8167,10 @@ export class OrganizationController extends Controller { }); toUpdate.push(current); + if (draftPos.next_holderId === null) { + await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); + } + // Track mapping for position sync posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); } else { @@ -8512,107 +8516,6 @@ export class OrganizationController extends Controller { }; } - /** - * Helper function: Sync positions for a PosMaster - * Handles DELETE/UPDATE/INSERT for positions associated with a posMaster - * - * @deprecated Kept as fallback - use syncAllPositionsBatch for better performance - */ - private async syncPositionsForPosMaster( - queryRunner: any, - draftPosMasterId: string, - currentPosMasterId: string, - _draftRevisionId: string, - _currentRevisionId: string, - nextHolderId: string | null | undefined, - ): Promise<{ deleted: number; updated: number; inserted: number }> { - // Fetch draft and current positions for this posMaster - const [draftPositions, currentPositions] = await Promise.all([ - queryRunner.manager.find(Position, { - where: { - posMasterId: draftPosMasterId, - }, - order: { orderNo: "ASC" }, - }), - queryRunner.manager.find(Position, { - where: { - posMasterId: currentPosMasterId, - }, - }), - ]); - - // If no draft positions, delete all current positions - if (draftPositions.length === 0) { - if (currentPositions.length > 0) { - await queryRunner.manager.delete( - Position, - currentPositions.map((p: any) => p.id), - ); - } - return { deleted: currentPositions.length, updated: 0, inserted: 0 }; - } - - // Build maps for tracking - const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); - - // DELETE: Current positions not in draft (by orderNo) - const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); - const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo)); - - if (toDelete.length > 0) { - await queryRunner.manager.delete( - Position, - toDelete.map((p: any) => p.id), - ); - } - - // UPDATE and INSERT - let updatedCount = 0; - let insertedCount = 0; - for (const draftPos of draftPositions) { - const current: any = currentByOrderNo.get(draftPos.orderNo); - - if (current) { - // UPDATE existing position - await queryRunner.manager.update(Position, current.id, { - positionName: draftPos.positionName, - positionField: draftPos.positionField, - posTypeId: draftPos.posTypeId, - posLevelId: draftPos.posLevelId, - posExecutiveId: draftPos.posExecutiveId, - positionExecutiveField: draftPos.positionExecutiveField, - positionArea: draftPos.positionArea, - isSpecial: draftPos.isSpecial, - orderNo: draftPos.orderNo, - positionIsSelected: draftPos.positionIsSelected, - lastUpdateFullName: draftPos.lastUpdateFullName, - lastUpdatedAt: new Date(), - }); - updatedCount++; - } else { - // INSERT new position - const newPosition = queryRunner.manager.create(Position, { - ...draftPos, - id: undefined, - posMasterId: currentPosMasterId, - }); - await queryRunner.manager.save(newPosition); - insertedCount++; - } - - // update profile - if (nextHolderId != null && draftPos.positionIsSelected) { - await queryRunner.manager.update(Profile, nextHolderId, { - position: draftPos.positionName, - posTypeId: draftPos.posTypeId, - posLevelId: draftPos.posLevelId, - }); - } - } - - return { deleted: toDelete.length, updated: updatedCount, inserted: insertedCount }; - } - /** * Batch version: Sync positions for ALL posMasters in a single operation * This significantly reduces database round trips for large organizations @@ -8674,6 +8577,7 @@ export class OrganizationController extends Controller { // Collect all operations const allToDelete: string[] = []; + const allToDeleteHistory: string[] = []; const allToUpdate: Array<{ id: string; data: any }> = []; const allToInsert: Array = []; const profileUpdates: Map = new Map(); @@ -8696,6 +8600,7 @@ export class OrganizationController extends Controller { // If no draft positions, mark all current positions for deletion if (draftPositions.length === 0) { allToDelete.push(...currentPositions.map((p: any) => p.id)); + allToDeleteHistory.push(...currentPositions.map((p: any) => p.ancestorDNA)); continue; } @@ -8707,6 +8612,7 @@ export class OrganizationController extends Controller { for (const currentPos of currentPositions) { if (!draftOrderNos.has(currentPos.orderNo)) { allToDelete.push(currentPos.id); + allToDeleteHistory.push(currentPos.ancestorDNA); } } @@ -8742,10 +8648,6 @@ export class OrganizationController extends Controller { }); } - if (nextHolderId === null) { - await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); - } - // Collect profile update for the selected position if (nextHolderId != null && draftPos.positionIsSelected) { profileUpdates.set(nextHolderId, { @@ -8753,10 +8655,8 @@ export class OrganizationController extends Controller { posTypeId: draftPos.posTypeId, posLevelId: draftPos.posLevelId, }); - } - // Collect history data for the selected position - if (nextHolderId != null && draftPos.positionIsSelected) { + // Collect history data for the selected position const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any; if (draftPosMaster && draftPosMaster.ancestorDNA) { // Find the selected position from draft positions @@ -8805,6 +8705,11 @@ export class OrganizationController extends Controller { // Bulk DELETE if (allToDelete.length > 0) { await queryRunner.manager.delete(Position, allToDelete); + await Promise.all( + allToDeleteHistory.map(async (ancestorDNA) => { + await SavePosMasterHistoryOfficer(queryRunner, ancestorDNA, null, null); + }), + ); deletedCount = allToDelete.length; } From f03ccb78ac6b5707264ac804e25f7ad603197cda Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 12 Feb 2026 12:10:14 +0700 Subject: [PATCH 076/178] Migrate move isDeleted in profileSalary --- src/entities/ProfileSalary.ts | 7 ------- src/entities/ProfileSalaryBackup.ts | 7 ------- src/entities/ProfileSalaryHistory.ts | 7 ------- ...ields_isDeleted_in_tables_profileSalary.ts | 19 +++++++++++++++++++ 4 files changed, 19 insertions(+), 21 deletions(-) create mode 100644 src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts diff --git a/src/entities/ProfileSalary.ts b/src/entities/ProfileSalary.ts index 1ab7acab..ddbe184b 100644 --- a/src/entities/ProfileSalary.ts +++ b/src/entities/ProfileSalary.ts @@ -287,13 +287,6 @@ export class ProfileSalary extends EntityBase { }) positionArea: string; - @Column({ - nullable: false, - comment: "สถานะลบข้อมูล", - default: false, - }) - isDeleted: boolean; - @ManyToOne(() => Command, (command) => command.profileSalarys) @JoinColumn({ name: "commandId" }) command: Command; diff --git a/src/entities/ProfileSalaryBackup.ts b/src/entities/ProfileSalaryBackup.ts index 1274e319..29cdd325 100644 --- a/src/entities/ProfileSalaryBackup.ts +++ b/src/entities/ProfileSalaryBackup.ts @@ -292,11 +292,4 @@ export class ProfileSalaryBackup extends EntityBase { }) positionArea: string; - @Column({ - nullable: false, - comment: "สถานะลบข้อมูล", - default: false, - }) - isDeleted: boolean; - } \ No newline at end of file diff --git a/src/entities/ProfileSalaryHistory.ts b/src/entities/ProfileSalaryHistory.ts index b5482c29..ea6af033 100644 --- a/src/entities/ProfileSalaryHistory.ts +++ b/src/entities/ProfileSalaryHistory.ts @@ -228,13 +228,6 @@ export class ProfileSalaryHistory extends EntityBase { }) posNumCodeSitAbb: string; - @Column({ - nullable: false, - comment: "สถานะลบข้อมูล", - default: false, - }) - isDeleted: boolean; - @ManyToOne(() => Command, (command) => command.profileSalaryHistorys) @JoinColumn({ name: "commandId" }) command: Command; diff --git a/src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts b/src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts new file mode 100644 index 00000000..31d4a79c --- /dev/null +++ b/src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MoveFieldsIsDeletedInTablesProfileSalary1770872734016 implements MigrationInterface { + name = 'MoveFieldsIsDeletedInTablesProfileSalary1770872734016' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileSalaryHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileSalary\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileSalaryBackup\` DROP COLUMN \`isDeleted\``); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileSalaryBackup\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE \`profileSalary\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE \`profileSalaryHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT '0'`); + + } + +} From 64aca4f5fa82b211c1a583a24f3fc963b2df599b Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 12 Feb 2026 12:10:14 +0700 Subject: [PATCH 077/178] Migrate move isDeleted in profileSalary --- src/entities/ProfileSalary.ts | 7 ------- src/entities/ProfileSalaryBackup.ts | 7 ------- src/entities/ProfileSalaryHistory.ts | 7 ------- ...ields_isDeleted_in_tables_profileSalary.ts | 19 +++++++++++++++++++ 4 files changed, 19 insertions(+), 21 deletions(-) create mode 100644 src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts diff --git a/src/entities/ProfileSalary.ts b/src/entities/ProfileSalary.ts index 1ab7acab..ddbe184b 100644 --- a/src/entities/ProfileSalary.ts +++ b/src/entities/ProfileSalary.ts @@ -287,13 +287,6 @@ export class ProfileSalary extends EntityBase { }) positionArea: string; - @Column({ - nullable: false, - comment: "สถานะลบข้อมูล", - default: false, - }) - isDeleted: boolean; - @ManyToOne(() => Command, (command) => command.profileSalarys) @JoinColumn({ name: "commandId" }) command: Command; diff --git a/src/entities/ProfileSalaryBackup.ts b/src/entities/ProfileSalaryBackup.ts index 1274e319..29cdd325 100644 --- a/src/entities/ProfileSalaryBackup.ts +++ b/src/entities/ProfileSalaryBackup.ts @@ -292,11 +292,4 @@ export class ProfileSalaryBackup extends EntityBase { }) positionArea: string; - @Column({ - nullable: false, - comment: "สถานะลบข้อมูล", - default: false, - }) - isDeleted: boolean; - } \ No newline at end of file diff --git a/src/entities/ProfileSalaryHistory.ts b/src/entities/ProfileSalaryHistory.ts index b5482c29..ea6af033 100644 --- a/src/entities/ProfileSalaryHistory.ts +++ b/src/entities/ProfileSalaryHistory.ts @@ -228,13 +228,6 @@ export class ProfileSalaryHistory extends EntityBase { }) posNumCodeSitAbb: string; - @Column({ - nullable: false, - comment: "สถานะลบข้อมูล", - default: false, - }) - isDeleted: boolean; - @ManyToOne(() => Command, (command) => command.profileSalaryHistorys) @JoinColumn({ name: "commandId" }) command: Command; diff --git a/src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts b/src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts new file mode 100644 index 00000000..31d4a79c --- /dev/null +++ b/src/migration/1770872734016-move_fields_isDeleted_in_tables_profileSalary.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MoveFieldsIsDeletedInTablesProfileSalary1770872734016 implements MigrationInterface { + name = 'MoveFieldsIsDeletedInTablesProfileSalary1770872734016' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileSalaryHistory\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileSalary\` DROP COLUMN \`isDeleted\``); + await queryRunner.query(`ALTER TABLE \`profileSalaryBackup\` DROP COLUMN \`isDeleted\``); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileSalaryBackup\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE \`profileSalary\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE \`profileSalaryHistory\` ADD \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT '0'`); + + } + +} From ef17236eb0a2dc3e117a3fcf40d3705b0667e327 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 13:16:43 +0700 Subject: [PATCH 078/178] fix: bug save posMasterHistory, tuning performance script --- src/controllers/OrganizationController.ts | 59 +++++++++---- ...70875727560-add_indexes_for_performance.ts | 62 ++++++++++++++ src/services/PositionService.ts | 83 +++++++++++++++++++ 3 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 src/migration/1770875727560-add_indexes_for_performance.ts diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index b0a007f5..51159b91 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -53,6 +53,7 @@ import { } from "../keycloak"; // import { getPositionCounts, getCounts, getRootCounts } from "../services/OrganizationService"; import { + BatchSavePosMasterHistoryOfficer, CreatePosMasterHistoryEmployee, CreatePosMasterHistoryOfficer, SavePosMasterHistoryOfficer, @@ -8062,9 +8063,26 @@ export class OrganizationController extends Controller { // Clear current_holderId for positions that will have new holders const nextHolderIds = posMasterDraft .filter((x) => x.next_holderId != null) - .map((x) => x.next_holderId); + .map((x) => x.next_holderId) as string[]; if (nextHolderIds.length > 0) { + // FIX: Fetch positions first before updating (to avoid race condition) + const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { + where: { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds), + }, + }); + + // Save history BEFORE clearing current_holderId + const historyOps = posMastersToUpdate.map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); + + // Now clear current_holderId await queryRunner.manager.update( PosMaster, { @@ -8112,11 +8130,12 @@ export class OrganizationController extends Controller { // Then delete posMaster records await queryRunner.manager.delete(PosMaster, toDeleteIds); - await Promise.all( - toDelete.map(async (pos) => { - await SavePosMasterHistoryOfficer(queryRunner, pos.ancestorDNA, null, null); - }), - ); + const deleteHistoryOps = toDelete.map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); } // 2.4 Process draft positions (UPDATE or INSERT) @@ -8705,17 +8724,18 @@ export class OrganizationController extends Controller { // Bulk DELETE if (allToDelete.length > 0) { await queryRunner.manager.delete(Position, allToDelete); - await Promise.all( - allToDeleteHistory.map(async (ancestorDNA) => { - await SavePosMasterHistoryOfficer(queryRunner, ancestorDNA, null, null); - }), - ); + const deleteOps = allToDeleteHistory.map((ancestorDNA) => ({ + posMasterDnaId: ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, deleteOps); deletedCount = allToDelete.length; } - // Bulk UPDATE (batch by 500 to avoid query size limits) + // Bulk UPDATE (batch by 100 to avoid query size limits) if (allToUpdate.length > 0) { - const batchSize = 500; + const batchSize = 100; for (let i = 0; i < allToUpdate.length; i += batchSize) { const batch = allToUpdate.slice(i, i + batchSize); await Promise.all( @@ -8727,7 +8747,7 @@ export class OrganizationController extends Controller { // Bulk INSERT if (allToInsert.length > 0) { - const batchSize = 500; + const batchSize = 100; for (let i = 0; i < allToInsert.length; i += batchSize) { const batch = allToInsert.slice(i, i + batchSize); await queryRunner.manager.save(Position, batch); @@ -8747,10 +8767,13 @@ export class OrganizationController extends Controller { // Save PosMasterHistory for updated positions if (historyCalls.length > 0) { - await Promise.all( - historyCalls.map(({ ancestorDNA, profileId, historyData }) => - SavePosMasterHistoryOfficer(queryRunner, ancestorDNA, profileId, historyData), - ), + await BatchSavePosMasterHistoryOfficer( + queryRunner, + historyCalls.map(({ ancestorDNA, profileId, historyData }) => ({ + posMasterDnaId: ancestorDNA, + profileId, + pm: historyData, + })), ); } diff --git a/src/migration/1770875727560-add_indexes_for_performance.ts b/src/migration/1770875727560-add_indexes_for_performance.ts new file mode 100644 index 00000000..c08e82ab --- /dev/null +++ b/src/migration/1770875727560-add_indexes_for_performance.ts @@ -0,0 +1,62 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddIndexesForPerformance1770875727560 implements MigrationInterface { + name = "AddIndexesForPerformance1770875727560"; + + public async up(queryRunner: QueryRunner): Promise { + // Index for posMasterHistory lookups + await queryRunner.query(` + CREATE INDEX IDX_posMasterHistory_ancestorDNA + ON posMasterHistory(ancestorDNA, createdAt DESC) + `); + + // Index for org tables lookups + await queryRunner.query(` + CREATE INDEX IDX_orgRoot_ancestorDNA_revision + ON orgRoot(ancestorDNA, orgRevisionId) + `); + + await queryRunner.query(` + CREATE INDEX IDX_orgChild1_ancestorDNA_revision + ON orgChild1(ancestorDNA, orgRevisionId) + `); + + await queryRunner.query(` + CREATE INDEX IDX_orgChild2_ancestorDNA_revision + ON orgChild2(ancestorDNA, orgRevisionId) + `); + + await queryRunner.query(` + CREATE INDEX IDX_orgChild3_ancestorDNA_revision + ON orgChild3(ancestorDNA, orgRevisionId) + `); + + await queryRunner.query(` + CREATE INDEX IDX_orgChild4_ancestorDNA_revision + ON orgChild4(ancestorDNA, orgRevisionId) + `); + + // Index for posMaster lookups + await queryRunner.query(` + CREATE INDEX IDX_posMaster_revision_org + ON posMaster(orgRevisionId, orgRootId, orgChild1Id, orgChild2Id, orgChild3Id, orgChild4Id) + `); + + await queryRunner.query(` + CREATE INDEX IDX_posMaster_ancestorDNA + ON posMaster(ancestorDNA) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IDX_position_posMasterId ON position`); + await queryRunner.query(`DROP INDEX IDX_posMaster_ancestorDNA ON posMaster`); + await queryRunner.query(`DROP INDEX IDX_posMaster_revision_org ON posMaster`); + await queryRunner.query(`DROP INDEX IDX_orgChild4_ancestorDNA_revision ON orgChild4`); + await queryRunner.query(`DROP INDEX IDX_orgChild3_ancestorDNA_revision ON orgChild3`); + await queryRunner.query(`DROP INDEX IDX_orgChild2_ancestorDNA_revision ON orgChild2`); + await queryRunner.query(`DROP INDEX IDX_orgChild1_ancestorDNA_revision ON orgChild1`); + await queryRunner.query(`DROP INDEX IDX_orgRoot_ancestorDNA_revision ON orgRoot`); + await queryRunner.query(`DROP INDEX IDX_posMasterHistory_ancestorDNA ON posMasterHistory`); + } +} diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 34631666..64fb58a4 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -1,3 +1,4 @@ +import { In } from "typeorm"; import { SavePosMasterHistory } from "./../interfaces/OrgMapping"; import { AppDataSource } from "../database/data-source"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; @@ -320,3 +321,85 @@ export async function SavePosMasterHistoryOfficer( return false; } } + +export interface BatchPosMasterHistoryOperation { + posMasterDnaId: string; + profileId: string | null; + pm: SavePosMasterHistory | null; +} + +export async function BatchSavePosMasterHistoryOfficer( + queryRunner: any, + operations: BatchPosMasterHistoryOperation[], +): Promise { + if (operations.length === 0) return true; + + try { + const repoPosMasterHistory = queryRunner.manager.getRepository(PosMasterHistory); + const dnaIds = operations.map((op) => op.posMasterDnaId); + + // Fetch all existing history records in ONE query + const existingHistory = await repoPosMasterHistory.find({ + where: { ancestorDNA: In(dnaIds) }, + order: { createdAt: "DESC" }, + }); + + // Build lookup map + const historyByDna = new Map(); + for (const h of existingHistory) { + if (!historyByDna.has(h.ancestorDNA)) { + historyByDna.set(h.ancestorDNA, []); + } + historyByDna.get(h.ancestorDNA)!.push(h); + } + + // Process operations and collect new records + const newRecords: PosMasterHistory[] = []; + const _null: any = null; + + for (const op of operations) { + const existing = historyByDna.get(op.posMasterDnaId)?.[0]; + const shouldInsert = !existing && op.profileId && op.pm; + const profileChanged = existing && existing.profileId !== op.profileId; + + if (shouldInsert || profileChanged) { + const newPmh = new PosMasterHistory(); + newPmh.ancestorDNA = op.posMasterDnaId; + newPmh.prefix = op.pm?.prefix ?? _null; + newPmh.firstName = op.pm?.firstName ?? _null; + newPmh.lastName = op.pm?.lastName ?? _null; + newPmh.position = op.pm?.position ?? _null; + newPmh.posType = op.pm?.posType ?? _null; + newPmh.posLevel = op.pm?.posLevel ?? _null; + newPmh.posExecutive = op.pm?.posExecutive ?? _null; + newPmh.profileId = op.profileId ?? _null; + newPmh.rootDnaId = op.pm?.rootDnaId ?? _null; + newPmh.child1DnaId = op.pm?.child1DnaId ?? _null; + newPmh.child2DnaId = op.pm?.child2DnaId ?? _null; + newPmh.child3DnaId = op.pm?.child3DnaId ?? _null; + newPmh.child4DnaId = op.pm?.child4DnaId ?? _null; + newPmh.shortName = op.pm?.shortName ?? _null; + newPmh.posMasterNoPrefix = op.pm?.posMasterNoPrefix ?? _null; + newPmh.posMasterNo = op.pm?.posMasterNo ?? _null; + newPmh.posMasterNoSuffix = op.pm?.posMasterNoSuffix ?? _null; + newPmh.createdUserId = "system"; + newPmh.createdFullName = "system"; + newPmh.lastUpdateUserId = "system"; + newPmh.lastUpdateFullName = "system"; + newPmh.createdAt = new Date(); + newPmh.lastUpdatedAt = new Date(); + newRecords.push(newPmh); + } + } + + // Batch insert all new records + if (newRecords.length > 0) { + await queryRunner.manager.save(PosMasterHistory, newRecords); + } + + return true; + } catch (err) { + console.error("BatchSavePosMasterHistoryOfficer error:", err); + return false; + } +} From 82ecf2cb81280370c92544d6d8fb17ebbac92677 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 14:06:07 +0700 Subject: [PATCH 079/178] fix: save posMasterHistory null --- src/controllers/OrganizationController.ts | 585 +++++++++++----------- 1 file changed, 302 insertions(+), 283 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 51159b91..c7e69071 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7952,312 +7952,331 @@ export class OrganizationController extends Controller { if (!orgRootDraft) return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้างร่าง"); // Part 1: Differential sync of organization structure (bottom-up) // Build mapping incrementally as we process each level - const allMappings: AllOrgMappings = { - orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, - }; - // Track sync statistics for organization nodes - const orgSyncStats: Record = - {}; + if (orgRootCurrent) { + const allMappings: AllOrgMappings = { + orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, + }; - // Process from top (Root) to bottom (Child4) to handle foreign key constraints - // OrgRoot (sync first - no parent dependencies) - const orgRootResult = await this.syncOrgLevel( - queryRunner, - OrgRoot, - this.orgRootRepository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgRoot = orgRootResult.mapping; - orgSyncStats.orgRoot = orgRootResult.counts; + // Track sync statistics for organization nodes + const orgSyncStats: Record = + {}; - // Child1 (parent OrgRoot already synced) - const child1Result = await this.syncOrgLevel( - queryRunner, - OrgChild1, - this.child1Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild1 = child1Result.mapping; - orgSyncStats.orgChild1 = child1Result.counts; + // Process from top (Root) to bottom (Child4) to handle foreign key constraints + // OrgRoot (sync first - no parent dependencies) + const orgRootResult = await this.syncOrgLevel( + queryRunner, + OrgRoot, + this.orgRootRepository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgRoot = orgRootResult.mapping; + orgSyncStats.orgRoot = orgRootResult.counts; - // Child2 (parents OrgRoot and Child1 already synced) - const child2Result = await this.syncOrgLevel( - queryRunner, - OrgChild2, - this.child2Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild2 = child2Result.mapping; - orgSyncStats.orgChild2 = child2Result.counts; + // Child1 (parent OrgRoot already synced) + const child1Result = await this.syncOrgLevel( + queryRunner, + OrgChild1, + this.child1Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild1 = child1Result.mapping; + orgSyncStats.orgChild1 = child1Result.counts; - // Child3 (parents OrgRoot, Child1, Child2 already synced) - const child3Result = await this.syncOrgLevel( - queryRunner, - OrgChild3, - this.child3Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild3 = child3Result.mapping; - orgSyncStats.orgChild3 = child3Result.counts; + // Child2 (parents OrgRoot and Child1 already synced) + const child2Result = await this.syncOrgLevel( + queryRunner, + OrgChild2, + this.child2Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild2 = child2Result.mapping; + orgSyncStats.orgChild2 = child2Result.counts; - // Child4 (parents OrgRoot, Child1, Child2, Child3 already synced) - const child4Result = await this.syncOrgLevel( - queryRunner, - OrgChild4, - this.child4Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild4 = child4Result.mapping; - orgSyncStats.orgChild4 = child4Result.counts; + // Child3 (parents OrgRoot, Child1, Child2 already synced) + const child3Result = await this.syncOrgLevel( + queryRunner, + OrgChild3, + this.child3Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild3 = child3Result.mapping; + orgSyncStats.orgChild3 = child3Result.counts; - // Part 2: Sync position data using new org IDs from Part 1 - // 2.1 Clear current_holderId for affected positions (keep existing logic) - // Get draft organization IDs under the given rootDnaId to find positions to clear - const draftOrgIds = { - orgRoot: [...allMappings.orgRoot.byDraftId.keys()], - orgChild1: [...allMappings.orgChild1.byDraftId.keys()], - orgChild2: [...allMappings.orgChild2.byDraftId.keys()], - orgChild3: [...allMappings.orgChild3.byDraftId.keys()], - orgChild4: [...allMappings.orgChild4.byDraftId.keys()], - }; + // Child4 (parents OrgRoot, Child1, Child2, Child3 already synced) + const child4Result = await this.syncOrgLevel( + queryRunner, + OrgChild4, + this.child4Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild4 = child4Result.mapping; + orgSyncStats.orgChild4 = child4Result.counts; - // Get draft positions that belong to any org under the rootDnaId - const posMasterDraft = await this.posMasterRepository.find({ - where: [ - { orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) }, - { orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) }, - { orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) }, - { orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) }, - { orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) }, - ], - }); + // Part 2: Sync position data using new org IDs from Part 1 + // 2.1 Clear current_holderId for affected positions (keep existing logic) + // Get draft organization IDs under the given rootDnaId to find positions to clear + const draftOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.keys()], + orgChild1: [...allMappings.orgChild1.byDraftId.keys()], + orgChild2: [...allMappings.orgChild2.byDraftId.keys()], + orgChild3: [...allMappings.orgChild3.byDraftId.keys()], + orgChild4: [...allMappings.orgChild4.byDraftId.keys()], + }; - if (posMasterDraft.length <= 0) - return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); - - // Clear current_holderId for positions that will have new holders - const nextHolderIds = posMasterDraft - .filter((x) => x.next_holderId != null) - .map((x) => x.next_holderId) as string[]; - - if (nextHolderIds.length > 0) { - // FIX: Fetch positions first before updating (to avoid race condition) - const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { - where: { - orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds), - }, + // Get draft positions that belong to any org under the rootDnaId + const posMasterDraft = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) }, + { orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) }, + { orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) }, + { orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) }, + { orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) }, + ], }); - // Save history BEFORE clearing current_holderId - const historyOps = posMastersToUpdate.map((pos) => ({ - posMasterDnaId: pos.ancestorDNA, - profileId: null, - pm: null, - })); - await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); + if (posMasterDraft.length <= 0) + return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); - // Now clear current_holderId - await queryRunner.manager.update( - PosMaster, - { - orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds), - }, - { current_holderId: null, isSit: false }, - ); - } + // Clear current_holderId for positions that will have new holders + const nextHolderIds = posMasterDraft + .filter((x) => x.next_holderId != null) + .map((x) => x.next_holderId) as string[]; - // 2.2 Fetch current positions for comparison - // Get current organization IDs from the mappings - const currentOrgIds = { - orgRoot: [...allMappings.orgRoot.byDraftId.values()], - orgChild1: [...allMappings.orgChild1.byDraftId.values()], - orgChild2: [...allMappings.orgChild2.byDraftId.values()], - orgChild3: [...allMappings.orgChild3.byDraftId.values()], - orgChild4: [...allMappings.orgChild4.byDraftId.values()], - }; - - const posMasterCurrent = await this.posMasterRepository.find({ - where: [ - { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, - { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, - { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, - { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, - { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, - ], - }); - - // Build lookup map - const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p])); - - // 2.3 Batch DELETE: positions in current but not in draft - const toDelete = posMasterCurrent.filter( - (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), - ); - - if (toDelete.length > 0) { - const toDeleteIds = toDelete.map((p) => p.id); - - // Cascade delete positions first - await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); - - // Then delete posMaster records - await queryRunner.manager.delete(PosMaster, toDeleteIds); - - const deleteHistoryOps = toDelete.map((pos) => ({ - posMasterDnaId: pos.ancestorDNA, - profileId: null, - pm: null, - })); - await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); - } - - // 2.4 Process draft positions (UPDATE or INSERT) - const toUpdate: PosMaster[] = []; - const toInsert: any[] = []; - - // Track draft PosMaster ID to current PosMaster ID mapping for position sync - // Type: Map - const posMasterMapping: Map = new Map(); - - for (const draftPos of posMasterDraft) { - const current = currentByDNA.get(draftPos.ancestorDNA); - - // Map organization IDs using new IDs from Part 1 - const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot); - const orgChild1Id = this.resolveOrgId(draftPos.orgChild1Id ?? null, allMappings.orgChild1); - const orgChild2Id = this.resolveOrgId(draftPos.orgChild2Id ?? null, allMappings.orgChild2); - const orgChild3Id = this.resolveOrgId(draftPos.orgChild3Id ?? null, allMappings.orgChild3); - const orgChild4Id = this.resolveOrgId(draftPos.orgChild4Id ?? null, allMappings.orgChild4); - - if (current) { - // UPDATE existing position - Object.assign(current, { - createdAt: draftPos.createdAt, - createdUserId: draftPos.createdUserId, - createdFullName: draftPos.createdFullName, - lastUpdatedAt: new Date(), - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - posMasterNoPrefix: draftPos.posMasterNoPrefix, - posMasterNoSuffix: draftPos.posMasterNoSuffix, - posMasterNo: draftPos.posMasterNo, - posMasterOrder: draftPos.posMasterOrder, - orgRootId, - orgChild1Id, - orgChild2Id, - orgChild3Id, - orgChild4Id, - current_holderId: draftPos.next_holderId, - isSit: draftPos.isSit, - reason: draftPos.reason, - isDirector: draftPos.isDirector, - isStaff: draftPos.isStaff, - positionSign: draftPos.positionSign, - statusReport: "DONE", - isCondition: draftPos.isCondition, - conditionReason: draftPos.conditionReason, - }); - toUpdate.push(current); - - if (draftPos.next_holderId === null) { - await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); - } - - // Track mapping for position sync - posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); - } else { - // INSERT new position - const newPosMaster = queryRunner.manager.create(PosMaster, { - ...draftPos, - id: undefined, - orgRevisionId: currentRevisionId, - orgRootId, - orgChild1Id, - orgChild2Id, - orgChild3Id, - orgChild4Id, - current_holderId: draftPos.next_holderId, - statusReport: "DONE", + if (nextHolderIds.length > 0) { + // FIX: Fetch positions first before updating (to avoid race condition) + const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { + where: { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds), + }, }); - toInsert.push(newPosMaster); + // Save history BEFORE clearing current_holderId + const historyOps = posMastersToUpdate + .filter((x) => x.orgRootId != orgRootCurrent?.id) + .map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); + + // Now clear current_holderId + await queryRunner.manager.update( + PosMaster, + { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds), + }, + { current_holderId: null, isSit: false }, + ); } - } - // Batch save updates and inserts - if (toUpdate.length > 0) { - await queryRunner.manager.save(toUpdate); - } - if (toInsert.length > 0) { - const saved = await queryRunner.manager.save(toInsert); + // 2.2 Fetch current positions for comparison + // Get current organization IDs from the mappings + const currentOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.values()], + orgChild1: [...allMappings.orgChild1.byDraftId.values()], + orgChild2: [...allMappings.orgChild2.byDraftId.values()], + orgChild3: [...allMappings.orgChild3.byDraftId.values()], + orgChild4: [...allMappings.orgChild4.byDraftId.values()], + }; - // Track mapping for newly inserted posMasters - // saved is an array, map each to its draft ID - if (Array.isArray(saved)) { - for (let i = 0; i < saved.length; i++) { - const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; - if (draftPos && saved[i]) { - posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]); + const posMasterCurrent = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, + { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, + { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, + { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, + { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, + ], + }); + + // Build lookup map + const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p])); + + // 2.3 Batch DELETE: positions in current but not in draft + const toDelete = posMasterCurrent.filter( + (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), + ); + + if (toDelete.length > 0) { + const toDeleteIds = toDelete.map((p) => p.id); + + // Cascade delete positions first + await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); + + // Then delete posMaster records + await queryRunner.manager.delete(PosMaster, toDeleteIds); + + const deleteHistoryOps = toDelete.map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); + } + + // 2.4 Process draft positions (UPDATE or INSERT) + const toUpdate: PosMaster[] = []; + const toInsert: any[] = []; + + // Track draft PosMaster ID to current PosMaster ID mapping for position sync + // Type: Map + const posMasterMapping: Map = new Map(); + + for (const draftPos of posMasterDraft) { + const current = currentByDNA.get(draftPos.ancestorDNA); + + // Map organization IDs using new IDs from Part 1 + const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot); + const orgChild1Id = this.resolveOrgId( + draftPos.orgChild1Id ?? null, + allMappings.orgChild1, + ); + const orgChild2Id = this.resolveOrgId( + draftPos.orgChild2Id ?? null, + allMappings.orgChild2, + ); + const orgChild3Id = this.resolveOrgId( + draftPos.orgChild3Id ?? null, + allMappings.orgChild3, + ); + const orgChild4Id = this.resolveOrgId( + draftPos.orgChild4Id ?? null, + allMappings.orgChild4, + ); + + if (current) { + // UPDATE existing position + Object.assign(current, { + createdAt: draftPos.createdAt, + createdUserId: draftPos.createdUserId, + createdFullName: draftPos.createdFullName, + lastUpdatedAt: new Date(), + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + posMasterNoPrefix: draftPos.posMasterNoPrefix, + posMasterNoSuffix: draftPos.posMasterNoSuffix, + posMasterNo: draftPos.posMasterNo, + posMasterOrder: draftPos.posMasterOrder, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + isSit: draftPos.isSit, + reason: draftPos.reason, + isDirector: draftPos.isDirector, + isStaff: draftPos.isStaff, + positionSign: draftPos.positionSign, + statusReport: "DONE", + isCondition: draftPos.isCondition, + conditionReason: draftPos.conditionReason, + }); + toUpdate.push(current); + + if (draftPos.next_holderId === null) { + await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); + } + + // Track mapping for position sync + posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); + } else { + // INSERT new position + const newPosMaster = queryRunner.manager.create(PosMaster, { + ...draftPos, + id: undefined, + orgRevisionId: currentRevisionId, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + statusReport: "DONE", + }); + + toInsert.push(newPosMaster); + } + } + + // Batch save updates and inserts + if (toUpdate.length > 0) { + await queryRunner.manager.save(toUpdate); + } + if (toInsert.length > 0) { + const saved = await queryRunner.manager.save(toInsert); + + // Track mapping for newly inserted posMasters + // saved is an array, map each to its draft ID + if (Array.isArray(saved)) { + for (let i = 0; i < saved.length; i++) { + const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; + if (draftPos && saved[i]) { + posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]); + } } } } + + // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) + const positionSyncStats = await this.syncAllPositionsBatch( + queryRunner, + posMasterMapping, + drafRevisionId, + currentRevisionId, + ); + + // Build comprehensive summary + const summary = { + message: "ย้ายโครงสร้างสำเร็จ", + organization: { + orgRoot: orgSyncStats.orgRoot, + orgChild1: orgSyncStats.orgChild1, + orgChild2: orgSyncStats.orgChild2, + orgChild3: orgSyncStats.orgChild3, + orgChild4: orgSyncStats.orgChild4, + }, + positionMaster: { + deleted: toDelete.length, + updated: toUpdate.length, + inserted: toInsert.length, + }, + position: positionSyncStats, + }; + + await queryRunner.commitTransaction(); + return new HttpSuccess(summary); } - // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) - const positionSyncStats = await this.syncAllPositionsBatch( - queryRunner, - posMasterMapping, - drafRevisionId, - currentRevisionId, - ); - - // Build comprehensive summary - const summary = { - message: "ย้ายโครงสร้างสำเร็จ", - organization: { - orgRoot: orgSyncStats.orgRoot, - orgChild1: orgSyncStats.orgChild1, - orgChild2: orgSyncStats.orgChild2, - orgChild3: orgSyncStats.orgChild3, - orgChild4: orgSyncStats.orgChild4, - }, - positionMaster: { - deleted: toDelete.length, - updated: toUpdate.length, - inserted: toInsert.length, - }, - position: positionSyncStats, - }; - - await queryRunner.commitTransaction(); - return new HttpSuccess(summary); + return new HttpSuccess({}); } catch (error) { console.error("Error moving draft to current:", error); await queryRunner.rollbackTransaction(); From d916334537f3beabb5acffdabc394b0ee580e180 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 15:39:16 +0700 Subject: [PATCH 080/178] fix bug: add ancestorDNA org all leavel --- src/controllers/OrgChild1Controller.ts | 3 +++ src/controllers/OrgChild2Controller.ts | 3 +++ src/controllers/OrgChild3Controller.ts | 3 +++ src/controllers/OrgChild4Controller.ts | 3 +++ src/controllers/OrgRootController.ts | 3 +++ 5 files changed, 15 insertions(+) diff --git a/src/controllers/OrgChild1Controller.ts b/src/controllers/OrgChild1Controller.ts index 7ba65f60..6e44af05 100644 --- a/src/controllers/OrgChild1Controller.ts +++ b/src/controllers/OrgChild1Controller.ts @@ -204,6 +204,9 @@ export class OrgChild1Controller { child1.orgChild1Order = order == null || order.orgChild1Order == null ? 1 : order.orgChild1Order + 1; await this.child1Repository.save(child1, { data: request }); + // update ancestorDNA = id row + child1.ancestorDNA = child1.id; + await this.child1Repository.save(child1, { data: request }); setLogDataDiff(request, { before, after: child1 }); return new HttpSuccess(); } diff --git a/src/controllers/OrgChild2Controller.ts b/src/controllers/OrgChild2Controller.ts index b0ff4b88..28ce564f 100644 --- a/src/controllers/OrgChild2Controller.ts +++ b/src/controllers/OrgChild2Controller.ts @@ -164,6 +164,9 @@ export class OrgChild2Controller extends Controller { child2.orgChild2Order = order == null || order.orgChild2Order == null ? 1 : order.orgChild2Order + 1; await this.child2Repository.save(child2, { data: request }); + // update ancestorDNA = id row + child2.ancestorDNA = child2.id; + await this.child2Repository.save(child2, { data: request }); setLogDataDiff(request, { before, after: child2 }); return new HttpSuccess(); } diff --git a/src/controllers/OrgChild3Controller.ts b/src/controllers/OrgChild3Controller.ts index 3ba365ae..4ed10804 100644 --- a/src/controllers/OrgChild3Controller.ts +++ b/src/controllers/OrgChild3Controller.ts @@ -132,6 +132,9 @@ export class OrgChild3Controller { child3.orgChild3Order = order == null || order.orgChild3Order == null ? 1 : order.orgChild3Order + 1; await this.child3Repository.save(child3, { data: request }); + // update ancestorDNA = id row + child3.ancestorDNA = child3.id; + await this.child3Repository.save(child3, { data: request }); setLogDataDiff(request, { before, after: child3 }); return new HttpSuccess(); } diff --git a/src/controllers/OrgChild4Controller.ts b/src/controllers/OrgChild4Controller.ts index a43b7234..e18c15f9 100644 --- a/src/controllers/OrgChild4Controller.ts +++ b/src/controllers/OrgChild4Controller.ts @@ -163,6 +163,9 @@ export class OrgChild4Controller extends Controller { child4.orgChild4Order = order == null || order.orgChild4Order == null ? 1 : order.orgChild4Order + 1; await this.child4Repository.save(child4, { data: request }); + // update ancestorDNA = id row + child4.ancestorDNA = child4.id; + await this.child4Repository.save(child4, { data: request }); setLogDataDiff(request, { before, after: child4 }); return new HttpSuccess(); diff --git a/src/controllers/OrgRootController.ts b/src/controllers/OrgRootController.ts index 9e6e428f..45bf1436 100644 --- a/src/controllers/OrgRootController.ts +++ b/src/controllers/OrgRootController.ts @@ -203,6 +203,9 @@ export class OrgRootController extends Controller { orgRoot.lastUpdatedAt = new Date(); orgRoot.orgRootOrder = order == null || order.orgRootOrder == null ? 1 : order.orgRootOrder + 1; await this.orgRootRepository.save(orgRoot, { data: request }); + // update ancestorDNA = id row + orgRoot.ancestorDNA = orgRoot.id; + await this.orgRootRepository.save(orgRoot, { data: request }); setLogDataDiff(request, { before, after: orgRoot }); return new HttpSuccess(); From 22fd9152bf66d1b40d22fbee03cec296cdba4af1 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 17:10:44 +0700 Subject: [PATCH 081/178] fix: new root --- src/controllers/OrganizationController.ts | 614 +++++++++++----------- 1 file changed, 306 insertions(+), 308 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index c7e69071..7a4eb03e 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7932,7 +7932,7 @@ export class OrganizationController extends Controller { const currentRevisionId = currentRevision.id; // ตรวจสอบว่ามี rootDnaId ในโครงสร้างร่าง และในโครงสร้างปัจจุบันหรือไม่ - const [orgRootDraft, orgRootCurrent] = await Promise.all([ + let [orgRootDraft, orgRootCurrent] = await Promise.all([ this.orgRootRepository.findOne({ where: { ancestorDNA: rootDnaId, @@ -7950,333 +7950,331 @@ export class OrganizationController extends Controller { ]); if (!orgRootDraft) return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้างร่าง"); + + // if current record not found, create new one + if (!orgRootCurrent) { + // Create new current record using draft's ID + const newCurrentRoot = queryRunner.manager.create(OrgRoot, { + ...orgRootDraft, + id: undefined, // Let database generate new ID + orgRevisionId: currentRevisionId, // Change to current revision + }); + + const savedRoot = await queryRunner.manager.save(OrgRoot, newCurrentRoot); + orgRootCurrent = savedRoot; // Use saved record for sync + } + // Part 1: Differential sync of organization structure (bottom-up) // Build mapping incrementally as we process each level - if (orgRootCurrent) { - const allMappings: AllOrgMappings = { - orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, - }; + const allMappings: AllOrgMappings = { + orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, + }; - // Track sync statistics for organization nodes - const orgSyncStats: Record = - {}; + // Track sync statistics for organization nodes + const orgSyncStats: Record = + {}; - // Process from top (Root) to bottom (Child4) to handle foreign key constraints - // OrgRoot (sync first - no parent dependencies) - const orgRootResult = await this.syncOrgLevel( - queryRunner, - OrgRoot, - this.orgRootRepository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgRoot = orgRootResult.mapping; - orgSyncStats.orgRoot = orgRootResult.counts; + // Process from top (Root) to bottom (Child4) to handle foreign key constraints + // OrgRoot (sync first - no parent dependencies) + const orgRootResult = await this.syncOrgLevel( + queryRunner, + OrgRoot, + this.orgRootRepository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgRoot = orgRootResult.mapping; + orgSyncStats.orgRoot = orgRootResult.counts; - // Child1 (parent OrgRoot already synced) - const child1Result = await this.syncOrgLevel( - queryRunner, - OrgChild1, - this.child1Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild1 = child1Result.mapping; - orgSyncStats.orgChild1 = child1Result.counts; + // Child1 (parent OrgRoot already synced) + const child1Result = await this.syncOrgLevel( + queryRunner, + OrgChild1, + this.child1Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild1 = child1Result.mapping; + orgSyncStats.orgChild1 = child1Result.counts; - // Child2 (parents OrgRoot and Child1 already synced) - const child2Result = await this.syncOrgLevel( - queryRunner, - OrgChild2, - this.child2Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild2 = child2Result.mapping; - orgSyncStats.orgChild2 = child2Result.counts; + // Child2 (parents OrgRoot and Child1 already synced) + const child2Result = await this.syncOrgLevel( + queryRunner, + OrgChild2, + this.child2Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild2 = child2Result.mapping; + orgSyncStats.orgChild2 = child2Result.counts; - // Child3 (parents OrgRoot, Child1, Child2 already synced) - const child3Result = await this.syncOrgLevel( - queryRunner, - OrgChild3, - this.child3Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild3 = child3Result.mapping; - orgSyncStats.orgChild3 = child3Result.counts; + // Child3 (parents OrgRoot, Child1, Child2 already synced) + const child3Result = await this.syncOrgLevel( + queryRunner, + OrgChild3, + this.child3Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild3 = child3Result.mapping; + orgSyncStats.orgChild3 = child3Result.counts; - // Child4 (parents OrgRoot, Child1, Child2, Child3 already synced) - const child4Result = await this.syncOrgLevel( - queryRunner, - OrgChild4, - this.child4Repository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); - allMappings.orgChild4 = child4Result.mapping; - orgSyncStats.orgChild4 = child4Result.counts; + // Child4 (parents OrgRoot, Child1, Child2, Child3 already synced) + const child4Result = await this.syncOrgLevel( + queryRunner, + OrgChild4, + this.child4Repository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + allMappings.orgChild4 = child4Result.mapping; + orgSyncStats.orgChild4 = child4Result.counts; - // Part 2: Sync position data using new org IDs from Part 1 - // 2.1 Clear current_holderId for affected positions (keep existing logic) - // Get draft organization IDs under the given rootDnaId to find positions to clear - const draftOrgIds = { - orgRoot: [...allMappings.orgRoot.byDraftId.keys()], - orgChild1: [...allMappings.orgChild1.byDraftId.keys()], - orgChild2: [...allMappings.orgChild2.byDraftId.keys()], - orgChild3: [...allMappings.orgChild3.byDraftId.keys()], - orgChild4: [...allMappings.orgChild4.byDraftId.keys()], - }; + // Part 2: Sync position data using new org IDs from Part 1 + // 2.1 Clear current_holderId for affected positions (keep existing logic) + // Get draft organization IDs under the given rootDnaId to find positions to clear + const draftOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.keys()], + orgChild1: [...allMappings.orgChild1.byDraftId.keys()], + orgChild2: [...allMappings.orgChild2.byDraftId.keys()], + orgChild3: [...allMappings.orgChild3.byDraftId.keys()], + orgChild4: [...allMappings.orgChild4.byDraftId.keys()], + }; - // Get draft positions that belong to any org under the rootDnaId - const posMasterDraft = await this.posMasterRepository.find({ - where: [ - { orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) }, - { orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) }, - { orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) }, - { orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) }, - { orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) }, - ], + // Get draft positions that belong to any org under the rootDnaId + const posMasterDraft = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) }, + { orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) }, + { orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) }, + { orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) }, + { orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) }, + ], + }); + + if (posMasterDraft.length <= 0) + return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); + + // Clear current_holderId for positions that will have new holders + const nextHolderIds = posMasterDraft + .filter((x) => x.next_holderId != null) + .map((x) => x.next_holderId) as string[]; + + if (nextHolderIds.length > 0) { + // FIX: Fetch positions first before updating (to avoid race condition) + const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { + where: { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds), + }, }); - if (posMasterDraft.length <= 0) - return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); - - // Clear current_holderId for positions that will have new holders - const nextHolderIds = posMasterDraft - .filter((x) => x.next_holderId != null) - .map((x) => x.next_holderId) as string[]; - - if (nextHolderIds.length > 0) { - // FIX: Fetch positions first before updating (to avoid race condition) - const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { - where: { - orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds), - }, - }); - - // Save history BEFORE clearing current_holderId - const historyOps = posMastersToUpdate - .filter((x) => x.orgRootId != orgRootCurrent?.id) - .map((pos) => ({ - posMasterDnaId: pos.ancestorDNA, - profileId: null, - pm: null, - })); - await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); - - // Now clear current_holderId - await queryRunner.manager.update( - PosMaster, - { - orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds), - }, - { current_holderId: null, isSit: false }, - ); - } - - // 2.2 Fetch current positions for comparison - // Get current organization IDs from the mappings - const currentOrgIds = { - orgRoot: [...allMappings.orgRoot.byDraftId.values()], - orgChild1: [...allMappings.orgChild1.byDraftId.values()], - orgChild2: [...allMappings.orgChild2.byDraftId.values()], - orgChild3: [...allMappings.orgChild3.byDraftId.values()], - orgChild4: [...allMappings.orgChild4.byDraftId.values()], - }; - - const posMasterCurrent = await this.posMasterRepository.find({ - where: [ - { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, - { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, - { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, - { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, - { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, - ], - }); - - // Build lookup map - const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p])); - - // 2.3 Batch DELETE: positions in current but not in draft - const toDelete = posMasterCurrent.filter( - (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), - ); - - if (toDelete.length > 0) { - const toDeleteIds = toDelete.map((p) => p.id); - - // Cascade delete positions first - await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); - - // Then delete posMaster records - await queryRunner.manager.delete(PosMaster, toDeleteIds); - - const deleteHistoryOps = toDelete.map((pos) => ({ + // Save history BEFORE clearing current_holderId + const historyOps = posMastersToUpdate + .filter((x) => x.orgRootId != orgRootCurrent?.id) + .map((pos) => ({ posMasterDnaId: pos.ancestorDNA, profileId: null, pm: null, })); - await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); - } + await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); - // 2.4 Process draft positions (UPDATE or INSERT) - const toUpdate: PosMaster[] = []; - const toInsert: any[] = []; - - // Track draft PosMaster ID to current PosMaster ID mapping for position sync - // Type: Map - const posMasterMapping: Map = new Map(); - - for (const draftPos of posMasterDraft) { - const current = currentByDNA.get(draftPos.ancestorDNA); - - // Map organization IDs using new IDs from Part 1 - const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot); - const orgChild1Id = this.resolveOrgId( - draftPos.orgChild1Id ?? null, - allMappings.orgChild1, - ); - const orgChild2Id = this.resolveOrgId( - draftPos.orgChild2Id ?? null, - allMappings.orgChild2, - ); - const orgChild3Id = this.resolveOrgId( - draftPos.orgChild3Id ?? null, - allMappings.orgChild3, - ); - const orgChild4Id = this.resolveOrgId( - draftPos.orgChild4Id ?? null, - allMappings.orgChild4, - ); - - if (current) { - // UPDATE existing position - Object.assign(current, { - createdAt: draftPos.createdAt, - createdUserId: draftPos.createdUserId, - createdFullName: draftPos.createdFullName, - lastUpdatedAt: new Date(), - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - posMasterNoPrefix: draftPos.posMasterNoPrefix, - posMasterNoSuffix: draftPos.posMasterNoSuffix, - posMasterNo: draftPos.posMasterNo, - posMasterOrder: draftPos.posMasterOrder, - orgRootId, - orgChild1Id, - orgChild2Id, - orgChild3Id, - orgChild4Id, - current_holderId: draftPos.next_holderId, - isSit: draftPos.isSit, - reason: draftPos.reason, - isDirector: draftPos.isDirector, - isStaff: draftPos.isStaff, - positionSign: draftPos.positionSign, - statusReport: "DONE", - isCondition: draftPos.isCondition, - conditionReason: draftPos.conditionReason, - }); - toUpdate.push(current); - - if (draftPos.next_holderId === null) { - await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); - } - - // Track mapping for position sync - posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); - } else { - // INSERT new position - const newPosMaster = queryRunner.manager.create(PosMaster, { - ...draftPos, - id: undefined, - orgRevisionId: currentRevisionId, - orgRootId, - orgChild1Id, - orgChild2Id, - orgChild3Id, - orgChild4Id, - current_holderId: draftPos.next_holderId, - statusReport: "DONE", - }); - - toInsert.push(newPosMaster); - } - } - - // Batch save updates and inserts - if (toUpdate.length > 0) { - await queryRunner.manager.save(toUpdate); - } - if (toInsert.length > 0) { - const saved = await queryRunner.manager.save(toInsert); - - // Track mapping for newly inserted posMasters - // saved is an array, map each to its draft ID - if (Array.isArray(saved)) { - for (let i = 0; i < saved.length; i++) { - const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; - if (draftPos && saved[i]) { - posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]); - } - } - } - } - - // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) - const positionSyncStats = await this.syncAllPositionsBatch( - queryRunner, - posMasterMapping, - drafRevisionId, - currentRevisionId, + // Now clear current_holderId + await queryRunner.manager.update( + PosMaster, + { + orgRevisionId: currentRevisionId, + current_holderId: In(nextHolderIds), + }, + { current_holderId: null, isSit: false }, ); - - // Build comprehensive summary - const summary = { - message: "ย้ายโครงสร้างสำเร็จ", - organization: { - orgRoot: orgSyncStats.orgRoot, - orgChild1: orgSyncStats.orgChild1, - orgChild2: orgSyncStats.orgChild2, - orgChild3: orgSyncStats.orgChild3, - orgChild4: orgSyncStats.orgChild4, - }, - positionMaster: { - deleted: toDelete.length, - updated: toUpdate.length, - inserted: toInsert.length, - }, - position: positionSyncStats, - }; - - await queryRunner.commitTransaction(); - return new HttpSuccess(summary); } - return new HttpSuccess({}); + // 2.2 Fetch current positions for comparison + // Get current organization IDs from the mappings + const currentOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.values()], + orgChild1: [...allMappings.orgChild1.byDraftId.values()], + orgChild2: [...allMappings.orgChild2.byDraftId.values()], + orgChild3: [...allMappings.orgChild3.byDraftId.values()], + orgChild4: [...allMappings.orgChild4.byDraftId.values()], + }; + + const posMasterCurrent = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, + { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, + { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, + { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, + { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, + ], + }); + + // Build lookup map + const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p])); + + // 2.3 Batch DELETE: positions in current but not in draft + const toDelete = posMasterCurrent.filter( + (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), + ); + + if (toDelete.length > 0) { + const toDeleteIds = toDelete.map((p) => p.id); + + // Cascade delete positions first + await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); + + // Then delete posMaster records + await queryRunner.manager.delete(PosMaster, toDeleteIds); + + const deleteHistoryOps = toDelete.map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); + } + + // 2.4 Process draft positions (UPDATE or INSERT) + const toUpdate: PosMaster[] = []; + const toInsert: any[] = []; + + // Track draft PosMaster ID to current PosMaster ID mapping for position sync + // Type: Map + const posMasterMapping: Map = new Map(); + + for (const draftPos of posMasterDraft) { + const current = currentByDNA.get(draftPos.ancestorDNA); + + // Map organization IDs using new IDs from Part 1 + const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot); + const orgChild1Id = this.resolveOrgId(draftPos.orgChild1Id ?? null, allMappings.orgChild1); + const orgChild2Id = this.resolveOrgId(draftPos.orgChild2Id ?? null, allMappings.orgChild2); + const orgChild3Id = this.resolveOrgId(draftPos.orgChild3Id ?? null, allMappings.orgChild3); + const orgChild4Id = this.resolveOrgId(draftPos.orgChild4Id ?? null, allMappings.orgChild4); + + if (current) { + // UPDATE existing position + Object.assign(current, { + createdAt: draftPos.createdAt, + createdUserId: draftPos.createdUserId, + createdFullName: draftPos.createdFullName, + lastUpdatedAt: new Date(), + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + posMasterNoPrefix: draftPos.posMasterNoPrefix, + posMasterNoSuffix: draftPos.posMasterNoSuffix, + posMasterNo: draftPos.posMasterNo, + posMasterOrder: draftPos.posMasterOrder, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + isSit: draftPos.isSit, + reason: draftPos.reason, + isDirector: draftPos.isDirector, + isStaff: draftPos.isStaff, + positionSign: draftPos.positionSign, + statusReport: "DONE", + isCondition: draftPos.isCondition, + conditionReason: draftPos.conditionReason, + }); + toUpdate.push(current); + + if (draftPos.next_holderId === null) { + await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); + } + + // Track mapping for position sync + posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]); + } else { + // INSERT new position + const newPosMaster = queryRunner.manager.create(PosMaster, { + ...draftPos, + id: undefined, + orgRevisionId: currentRevisionId, + orgRootId, + orgChild1Id, + orgChild2Id, + orgChild3Id, + orgChild4Id, + current_holderId: draftPos.next_holderId, + statusReport: "DONE", + }); + + toInsert.push(newPosMaster); + } + } + + // Batch save updates and inserts + if (toUpdate.length > 0) { + await queryRunner.manager.save(toUpdate); + } + if (toInsert.length > 0) { + const saved = await queryRunner.manager.save(toInsert); + + // Track mapping for newly inserted posMasters + // saved is an array, map each to its draft ID + if (Array.isArray(saved)) { + for (let i = 0; i < saved.length; i++) { + const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; + if (draftPos && saved[i]) { + posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]); + } + } + } + } + + // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) + const positionSyncStats = await this.syncAllPositionsBatch( + queryRunner, + posMasterMapping, + drafRevisionId, + currentRevisionId, + ); + + // Build comprehensive summary + const summary = { + message: "ย้ายโครงสร้างสำเร็จ", + organization: { + orgRoot: orgSyncStats.orgRoot, + orgChild1: orgSyncStats.orgChild1, + orgChild2: orgSyncStats.orgChild2, + orgChild3: orgSyncStats.orgChild3, + orgChild4: orgSyncStats.orgChild4, + }, + positionMaster: { + deleted: toDelete.length, + updated: toUpdate.length, + inserted: toInsert.length, + }, + position: positionSyncStats, + }; + + await queryRunner.commitTransaction(); + return new HttpSuccess(summary); } catch (error) { console.error("Error moving draft to current:", error); await queryRunner.rollbackTransaction(); @@ -8465,7 +8463,7 @@ export class OrganizationController extends Controller { for (const draft of toInsert) { const newNode: any = queryRunner.manager.create(entityClass, { ...draft, - id: undefined, + id: draft.id, orgRevisionId: currentRevisionId, }); From 0f4bee448938e3109f75d5341bf77ece121ae083 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 18:06:46 +0700 Subject: [PATCH 082/178] fix script --- src/controllers/OrganizationController.ts | 38 ++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 7a4eb03e..9f1e1615 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7938,7 +7938,7 @@ export class OrganizationController extends Controller { ancestorDNA: rootDnaId, orgRevisionId: drafRevisionId, }, - select: ["id"], + // select: ["id"], }), this.orgRootRepository.findOne({ where: { @@ -7981,16 +7981,32 @@ export class OrganizationController extends Controller { // Process from top (Root) to bottom (Child4) to handle foreign key constraints // OrgRoot (sync first - no parent dependencies) - const orgRootResult = await this.syncOrgLevel( - queryRunner, - OrgRoot, - this.orgRootRepository, - drafRevisionId, - currentRevisionId, - allMappings, - orgRootDraft?.id, - orgRootCurrent?.id, - ); + // If we manually created orgRootCurrent, skip syncOrgLevel and set up mapping directly + // to avoid double insert (syncOrgLevel would try to insert again because IDs don't match) + let orgRootResult: { mapping: OrgIdMapping; counts: { deleted: number; updated: number; inserted: number } }; + if (orgRootCurrent && orgRootDraft && orgRootCurrent.ancestorDNA === orgRootDraft.ancestorDNA) { + // Manually created - set up mapping directly + const rootMapping: OrgIdMapping = { + byAncestorDNA: new Map([[orgRootDraft.ancestorDNA, orgRootCurrent.id]]), + byDraftId: new Map([[orgRootDraft.id, orgRootCurrent.id]]), + }; + orgRootResult = { + mapping: rootMapping, + counts: { deleted: 0, updated: 0, inserted: 1 }, // Count as insert since we created it + }; + } else { + // Not manually created - use normal syncOrgLevel flow + orgRootResult = await this.syncOrgLevel( + queryRunner, + OrgRoot, + this.orgRootRepository, + drafRevisionId, + currentRevisionId, + allMappings, + orgRootDraft?.id, + orgRootCurrent?.id, + ); + } allMappings.orgRoot = orgRootResult.mapping; orgSyncStats.orgRoot = orgRootResult.counts; From d555c70af9386b5364be9b9e225edc9453c547c5 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 12 Feb 2026 18:18:29 +0700 Subject: [PATCH 083/178] fix: script insert --- src/controllers/OrganizationController.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 9f1e1615..d1b47518 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7983,8 +7983,15 @@ export class OrganizationController extends Controller { // OrgRoot (sync first - no parent dependencies) // If we manually created orgRootCurrent, skip syncOrgLevel and set up mapping directly // to avoid double insert (syncOrgLevel would try to insert again because IDs don't match) - let orgRootResult: { mapping: OrgIdMapping; counts: { deleted: number; updated: number; inserted: number } }; - if (orgRootCurrent && orgRootDraft && orgRootCurrent.ancestorDNA === orgRootDraft.ancestorDNA) { + let orgRootResult: { + mapping: OrgIdMapping; + counts: { deleted: number; updated: number; inserted: number }; + }; + if ( + orgRootCurrent && + orgRootDraft && + orgRootCurrent.ancestorDNA === orgRootDraft.ancestorDNA + ) { // Manually created - set up mapping directly const rootMapping: OrgIdMapping = { byAncestorDNA: new Map([[orgRootDraft.ancestorDNA, orgRootCurrent.id]]), @@ -8479,7 +8486,7 @@ export class OrganizationController extends Controller { for (const draft of toInsert) { const newNode: any = queryRunner.manager.create(entityClass, { ...draft, - id: draft.id, + id: undefined, orgRevisionId: currentRevisionId, }); From 9a1acc0b7d600aea3e7a2370ffce4d68f766af8d Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 12 Feb 2026 18:31:24 +0700 Subject: [PATCH 084/178] =?UTF-8?q?=E0=B8=AD=E0=B8=B1=E0=B8=9B=E0=B9=80?= =?UTF-8?q?=E0=B8=94=E0=B8=95=E0=B8=88=E0=B8=B3=E0=B8=99=E0=B8=A7=E0=B8=99?= =?UTF-8?q?=E0=B8=AA=E0=B8=B4=E0=B8=97=E0=B8=98=E0=B8=B4=E0=B9=8C=E0=B8=81?= =?UTF-8?q?=E0=B8=B2=E0=B8=A3=E0=B8=A5=E0=B8=B2=E0=B9=80=E0=B8=A1=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=9C=E0=B9=88=E0=B8=B2=E0=B8=99=E0=B8=97?= =?UTF-8?q?=E0=B8=94=E0=B8=A5=E0=B8=AD=E0=B8=87=E0=B8=87=E0=B8=B2=E0=B8=99?= =?UTF-8?q?=E0=B8=AF=20#2304?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 94 +++++++++++++++++++--------- src/interfaces/call-api.ts | 46 ++++++++++++++ 2 files changed, 112 insertions(+), 28 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index ece19088..90ea9a38 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -99,6 +99,7 @@ import { CreatePosMasterHistoryOfficer, } from "../services/PositionService"; import { PostRetireToExprofile } from "./ExRetirementController"; +import { LeaveType } from "../entities/LeaveType" @Route("api/v1/org/command") @Tags("Command") @Security("bearerAuth") @@ -154,7 +155,7 @@ export class CommandController extends Controller { private insigniaHistoryRepo = AppDataSource.getRepository(ProfileInsigniaHistory); private genderRepo = AppDataSource.getRepository(Gender); private avatarRepository = AppDataSource.getRepository(ProfileAvatar); - + private leaveType = AppDataSource.getRepository(LeaveType); /** * API list รายการคำสั่ง * @@ -5766,10 +5767,15 @@ export class CommandController extends Controller { ) { let _posNumCodeSit: string = ""; let _posNumCodeSitAbb: string = ""; + let commandType: any = "" const _command = await this.commandRepository.findOne({ where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, }); if (_command) { + commandType = await this.commandTypeRepository.findOne({ + select: { code: true }, + where: { id: _command.commandTypeId } + }); if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { const orgRootDeputy = await this.orgRootRepository.findOne({ where: { @@ -5807,11 +5813,15 @@ export class CommandController extends Controller { .orgRootShortName ?? ""; } } + const leaveType = await this.leaveType.findOne({ + select:{ id: true, limit: true, code: true }, + where:{ code: "LV-005" } + }); await Promise.all( body.data.map(async (item) => { const profile = await this.profileRepository.findOne({ relations: [ - "profileSalary", + // "profileSalary", "posType", "posLevel", "current_holders", @@ -5822,16 +5832,21 @@ export class CommandController extends Controller { "current_holders.orgChild4", ], where: { id: item.profileId }, - order: { - profileSalary: { - order: "DESC", - }, - }, + // order: { + // profileSalary: { + // order: "DESC", + // }, + // }, }); if (!profile) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); } - + const lastSalary = await this.salaryRepo.findOne({ + where: { profileId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; const orgRevision = await this.orgRevisionRepo.findOne({ where: { orgRevisionIsCurrent: true, @@ -5892,12 +5907,13 @@ export class CommandController extends Controller { amountSpecial: item.amountSpecial ? item.amountSpecial : null, positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - order: - profile.profileSalary.length >= 0 - ? profile.profileSalary.length > 0 - ? profile.profileSalary[0].order + 1 - : 1 - : null, + // order: + // profile.profileSalary.length >= 0 + // ? profile.profileSalary.length > 0 + // ? profile.profileSalary[0].order + 1 + // : 1 + // : null, + order: nextOrder, orgRoot: orgRootRef?.orgRootName ?? null, orgChild1: orgChild1Ref?.orgChild1Name ?? null, orgChild2: orgChild2Ref?.orgChild2Name ?? null, @@ -5928,19 +5944,41 @@ export class CommandController extends Controller { await this.salaryHistoryRepo.save(history); }), ); - const checkCommandType = await this.commandRepository.findOne({ - where: { id: body.data.length > 0 ? body.data[0].commandId?.toString() : "" }, - relations: ["commandType"], - }); - if (checkCommandType?.commandType.code == "C-PM-11") { - const profile = await this.profileRepository.find({ - where: { id: In(body.data.map((x) => x.profileId)) }, - }); - const data = profile.map((x) => ({ - ...x, - isProbation: false, - })); - await this.profileRepository.save(data); + // const checkCommandType = await this.commandRepository.findOne({ + // where: { id: body.data.length > 0 ? body.data[0].commandId?.toString() : "" }, + // relations: ["commandType"], + // }); + if (commandType && String(commandType.code) == "C-PM-11") { + // const profile = await this.profileRepository.find({ + // where: { id: In(body.data.map((x) => x.profileId)) }, + // }); + // const data = profile.map((x) => ({ + // ...x, + // isProbation: false, + // })); + // await this.profileRepository.save(data); + const profileIds = body.data.map((x) => x.profileId); + await this.profileRepository.update( + { id: In(profileIds) }, + { isProbation: false } + ); + // Task #2304 อัปเดตจำนวนสิทธิ์การลา เมื่อผ่านทดลองงานฯ + if (leaveType != null) { + await Promise.all( + body.data.map((item) => + new CallAPI().PutData(req, `/leave-beginning/schedule`, { + profileId: item.profileId, + leaveTypeId: leaveType.id, + leaveYear: item.commandYear, + leaveDays: leaveType.limit, + leaveDaysUsed: 0, + leaveCount: 0, + beginningLeaveDays: 0, + beginningLeaveCount: 0, + }) + ) + ); + } } return new HttpSuccess(); } @@ -6461,7 +6499,7 @@ export class CommandController extends Controller { await this.salaryHistoryRepo.save(history, { data: req }); if (profileEmployee.profileInsignias.length > 0) { - _oldInsigniaIds = profileEmployee.profileInsignias.map((x: any) => x.id); + _oldInsigniaIds = profileEmployee.profileInsignias.filter().map((x: any) => x.id); } await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE"); if (profileEmployee.keycloak != null) { diff --git a/src/interfaces/call-api.ts b/src/interfaces/call-api.ts index 398246c9..16986335 100644 --- a/src/interfaces/call-api.ts +++ b/src/interfaces/call-api.ts @@ -100,6 +100,52 @@ class CallAPI { } } + // Put + public async PutData(request: any, @Path() path: any, sendData: any, log = true) { + const token = "Bearer " + request.headers.authorization.replace("Bearer ", ""); + const url = process.env.API_URL + path; + + try { + const response = await axios.put(url, sendData, { + headers: { + Authorization: `${token}`, + "Content-Type": "application/json", + api_key: process.env.API_KEY, + }, + }); + + if (log) + addLogSequence(request, { + action: "request", + status: "success", + description: "connected", + request: { + method: "PUT", + url: url, + payload: JSON.stringify(sendData), + response: JSON.stringify(response.data.result), + }, + }); + + return response.data.result; + } catch (error) { + if (log) + addLogSequence(request, { + action: "request", + status: "error", + description: "unconnected", + request: { + method: "PUT", + url: url, + payload: JSON.stringify(sendData), + response: JSON.stringify(error), + }, + }); + + throw error; + } + } + //Delete File public async DeleteFile(request: any, @Path() path: any) { const token = "Bearer " + request.headers.authorization.replace("Bearer ", ""); From cfd5ced28af9e2980b1979d8a862ff6b351f3e38 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 13 Feb 2026 09:29:31 +0700 Subject: [PATCH 085/178] feat: isAllRoot --- src/controllers/PosMasterActController.ts | 131 ++++++++++++++++++++-- src/controllers/PositionController.ts | 95 ---------------- 2 files changed, 124 insertions(+), 102 deletions(-) diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 7c63ede5..179b4791 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -17,10 +17,13 @@ import HttpStatusCode from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; import { PosMasterAct } from "../entities/PosMasterAct"; import { PosMaster } from "../entities/PosMaster"; -import { Brackets, LessThan, MoreThan } from "typeorm"; +import { Brackets, In, IsNull, LessThan, MoreThan, Not } from "typeorm"; +import permission from "../interfaces/permission"; import { OrgRevision } from "../entities/OrgRevision"; import Extension from "../interfaces/extension"; import { ProfileActposition } from "../entities/ProfileActposition"; +import { RequestWithUser } from "../middlewares/user"; +import { escape } from "querystring"; @Route("api/v1/org/pos/act") @Tags("PosMasterAct") @@ -89,6 +92,120 @@ export class PosMasterActController extends Controller { return new HttpSuccess(posMasterAct); } + + /** + * API ค้นหาตำแหน่งในระบบสมัครสอบ ขรก. + * + * @summary ค้นหาตำแหน่งในระบบสมัครสอบ ขรก. + * + */ + @Post("search") + async searchAct( + @Request() request: RequestWithUser, + @Body() + body: { + posmasterId: string; + isAll: boolean; + isAllRoot?: boolean; + page?: number; + pageSize?: number; + }, + ) { + await new permission().PermissionGet(request, "SYS_ACTING"); + const { + page = 1, + pageSize = 100, + } = body + const posMasterMain = await this.posMasterRepository.findOne({ + where: { id: body.posmasterId }, + relations: ["posMasterActs"], + }); + if (posMasterMain == null) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); + } + let posId: any = posMasterMain.posMasterActs.map((x) => x.posMasterChildId); + posId.push(body.posmasterId); + let typeCondition: any = {}; + if (!body.isAllRoot) { + if (body.isAll == true) { + typeCondition = { + orgRootId: posMasterMain.orgRootId, + orgChild1Id: posMasterMain.orgChild1Id, + orgChild2Id: posMasterMain.orgChild2Id, + orgChild3Id: posMasterMain.orgChild3Id, + orgChild4Id: posMasterMain.orgChild4Id, + current_holderId: Not(IsNull()), + id: Not(In(posId)), + }; + } else { + typeCondition = { + orgRootId: posMasterMain.orgRootId == null ? IsNull() : posMasterMain.orgRootId, + orgChild1Id: posMasterMain.orgChild1Id == null ? IsNull() : posMasterMain.orgChild1Id, + orgChild2Id: posMasterMain.orgChild2Id == null ? IsNull() : posMasterMain.orgChild2Id, + orgChild3Id: posMasterMain.orgChild3Id == null ? IsNull() : posMasterMain.orgChild3Id, + orgChild4Id: posMasterMain.orgChild4Id == null ? IsNull() : posMasterMain.orgChild4Id, + current_holderId: Not(IsNull()), + id: Not(In(posId)), + }; + } + } else { + typeCondition = { + orgRootId: posMasterMain.orgRootId, + current_holderId: Not(IsNull()), + id: Not(In(posId)), + }; + } + + const [posMaster, total] = await this.posMasterRepository.findAndCount({ + where: typeCondition, + relations: [ + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + "current_holder.posLevel", + "current_holder.posType", + ], + skip: (page - 1) * pageSize, + take: pageSize, + }); + + const data = await Promise.all( + posMaster + .sort((a, b) => a.posMasterOrder - b.posMasterOrder) + .map((item) => { + const shortName = + item.orgChild4 != null + ? `${item.orgChild4.orgChild4ShortName} ${item.posMasterNo}` + : item?.orgChild3 != null + ? `${item.orgChild3.orgChild3ShortName} ${item.posMasterNo}` + : item?.orgChild2 != null + ? `${item.orgChild2.orgChild2ShortName} ${item.posMasterNo}` + : item?.orgChild1 != null + ? `${item.orgChild1.orgChild1ShortName} ${item.posMasterNo}` + : item?.orgRoot != null + ? `${item.orgRoot.orgRootShortName} ${item.posMasterNo}` + : null; + return { + id: item.id, + citizenId: item.current_holder?.citizenId ?? null, + isDirector: item.isDirector ?? null, + prefix: item.current_holder?.prefix ?? null, + firstName: item.current_holder?.firstName ?? null, + lastName: item.current_holder?.lastName ?? null, + posLevel: item.current_holder?.posLevel?.posLevelName ?? null, + posType: item.current_holder?.posType?.posTypeName ?? null, + position: item.current_holder?.position ?? null, + posNo: shortName, + }; + }), + ); + return new HttpSuccess({ data: data, total }); + } + + /** * API ลบรักษาการในตำแหน่ง * @@ -498,12 +615,12 @@ export class PosMasterActController extends Controller { x.posMasterChild?.orgRoot?.orgRootShortName, ].find((name) => !!name) && x.posMasterChild?.posMasterNo ? `${[ - x.posMasterChild?.orgChild4?.orgChild4ShortName, - x.posMasterChild?.orgChild3?.orgChild3ShortName, - x.posMasterChild?.orgChild2?.orgChild2ShortName, - x.posMasterChild?.orgChild1?.orgChild1ShortName, - x.posMasterChild?.orgRoot?.orgRootShortName, - ].find((name) => !!name)} ${x.posMasterChild.posMasterNo}` + x.posMasterChild?.orgChild4?.orgChild4ShortName, + x.posMasterChild?.orgChild3?.orgChild3ShortName, + x.posMasterChild?.orgChild2?.orgChild2ShortName, + x.posMasterChild?.orgChild1?.orgChild1ShortName, + x.posMasterChild?.orgRoot?.orgRootShortName, + ].find((name) => !!name)} ${x.posMasterChild.posMasterNo}` : x.posMasterChild?.posMasterNo || null; const orgShortNameAct = [ diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 4df79e19..c2facbd4 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -4943,101 +4943,6 @@ export class PositionController extends Controller { return new HttpSuccess({ data: formattedData, total }); } - /** - * API ค้นหาตำแหน่งในระบบสมัครสอบ ขรก. - * - * @summary ค้นหาตำแหน่งในระบบสมัครสอบ ขรก. - * - */ - @Post("act/search") - async searchAct( - @Request() request: RequestWithUser, - @Body() - body: { - posmasterId: string; - isAll: boolean; - }, - ) { - await new permission().PermissionGet(request, "SYS_ACTING"); - const posMasterMain = await this.posMasterRepository.findOne({ - where: { id: body.posmasterId }, - relations: ["posMasterActs"], - }); - if (posMasterMain == null) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); - } - let posId: any = posMasterMain.posMasterActs.map((x) => x.posMasterChildId); - posId.push(body.posmasterId); - let typeCondition: any = {}; - if (body.isAll == true) { - typeCondition = { - orgRootId: posMasterMain.orgRootId, - orgChild1Id: posMasterMain.orgChild1Id, - orgChild2Id: posMasterMain.orgChild2Id, - orgChild3Id: posMasterMain.orgChild3Id, - orgChild4Id: posMasterMain.orgChild4Id, - current_holderId: Not(IsNull()), - id: Not(In(posId)), - }; - } else { - typeCondition = { - orgRootId: posMasterMain.orgRootId == null ? IsNull() : posMasterMain.orgRootId, - orgChild1Id: posMasterMain.orgChild1Id == null ? IsNull() : posMasterMain.orgChild1Id, - orgChild2Id: posMasterMain.orgChild2Id == null ? IsNull() : posMasterMain.orgChild2Id, - orgChild3Id: posMasterMain.orgChild3Id == null ? IsNull() : posMasterMain.orgChild3Id, - orgChild4Id: posMasterMain.orgChild4Id == null ? IsNull() : posMasterMain.orgChild4Id, - current_holderId: Not(IsNull()), - id: Not(In(posId)), - }; - } - - const posMaster = await this.posMasterRepository.find({ - where: typeCondition, - relations: [ - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - "current_holder.posLevel", - "current_holder.posType", - ], - }); - - const data = await Promise.all( - posMaster - .sort((a, b) => a.posMasterOrder - b.posMasterOrder) - .map((item) => { - const shortName = - item.orgChild4 != null - ? `${item.orgChild4.orgChild4ShortName} ${item.posMasterNo}` - : item?.orgChild3 != null - ? `${item.orgChild3.orgChild3ShortName} ${item.posMasterNo}` - : item?.orgChild2 != null - ? `${item.orgChild2.orgChild2ShortName} ${item.posMasterNo}` - : item?.orgChild1 != null - ? `${item.orgChild1.orgChild1ShortName} ${item.posMasterNo}` - : item?.orgRoot != null - ? `${item.orgRoot.orgRootShortName} ${item.posMasterNo}` - : null; - return { - id: item.id, - citizenId: item.current_holder?.citizenId ?? null, - isDirector: item.isDirector ?? null, - prefix: item.current_holder?.prefix ?? null, - firstName: item.current_holder?.firstName ?? null, - lastName: item.current_holder?.lastName ?? null, - posLevel: item.current_holder?.posLevel?.posLevelName ?? null, - posType: item.current_holder?.posType?.posTypeName ?? null, - position: item.current_holder?.position ?? null, - posNo: shortName, - }; - }), - ); - return new HttpSuccess(data); - } - /** * API บันทึกตำแหน่งใหม่ * From a16ae79c7e0a6405037558e7782ddf71dff83db4 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 13 Feb 2026 12:50:35 +0700 Subject: [PATCH 086/178] =?UTF-8?q?fix:=20=E0=B9=84=E0=B8=9F=E0=B8=A5?= =?UTF-8?q?=E0=B9=8C=E0=B9=81=E0=B8=99=E0=B8=9A=E0=B8=97=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=E0=B8=94=E0=B8=B2=E0=B8=A7=E0=B8=99=E0=B9=8C=E0=B9=82?= =?UTF-8?q?=E0=B8=AB=E0=B8=A5=E0=B8=94=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B9=84?= =?UTF-8?q?=E0=B8=94=E0=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interfaces/utils.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts index d3409187..128499ea 100644 --- a/src/interfaces/utils.ts +++ b/src/interfaces/utils.ts @@ -601,7 +601,7 @@ export async function PayloadSendNoti(commandId: string) { where: { id: commandId, }, - relations: ["commandType"], + relations: ["commandType", "commandRecives"], }); if (!_command || !_command.commandType) return ""; @@ -615,19 +615,18 @@ export async function PayloadSendNoti(commandId: string) { } let attachments = {} if (_command.commandType.isUploadAttachment === true) { - const _payloadAtt = { - name: _command && _command.commandType - ? `เอกสารแนบท้ายคำสั่ง${_command.commandType.name}` - : "", - url: `${process.env.API_URL}/salary/file/ระบบออกคำสั่ง/แนบท้าย/${commandId}/แนบท้าย`, + + const attachmentPayloads = _command.commandRecives.map((recive: any) => ({ + name: `เอกสารแนบท้ายคำสั่ง${_command.commandType.name} (${recive.prefix}${recive.firstName} ${recive.lastName})`, + url: `${process.env.API_URL}/salary/file/ระบบออกคำสั่ง/แนบท้าย/${commandId}/${recive.citizenId}/แนบท้าย`, isReport: true, isTemplate: false, - } + })); + attachments = { - attachments: [_payload, _payloadAtt] + attachments: [_payload, ...attachmentPayloads] }; - } - else { + } else { attachments = { attachments: [_payload] }; From 307be83574c04cdce976d7dd2af16be258164aa2 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 13 Feb 2026 15:10:01 +0700 Subject: [PATCH 087/178] fix: path url attachmentPayloads == 'sub-file' --- src/interfaces/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts index 128499ea..347f28af 100644 --- a/src/interfaces/utils.ts +++ b/src/interfaces/utils.ts @@ -618,7 +618,7 @@ export async function PayloadSendNoti(commandId: string) { const attachmentPayloads = _command.commandRecives.map((recive: any) => ({ name: `เอกสารแนบท้ายคำสั่ง${_command.commandType.name} (${recive.prefix}${recive.firstName} ${recive.lastName})`, - url: `${process.env.API_URL}/salary/file/ระบบออกคำสั่ง/แนบท้าย/${commandId}/${recive.citizenId}/แนบท้าย`, + url: `${process.env.API_URL}/salary/sub-file/ระบบออกคำสั่ง/แนบท้าย/${commandId}/${recive.citizenId}/แนบท้าย`, isReport: true, isTemplate: false, })); From 7029b18a97d44e08df371db6c8502233a302f129 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 13 Feb 2026 16:49:00 +0700 Subject: [PATCH 088/178] fix: error Invalid time value --- src/interfaces/extension.ts | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/interfaces/extension.ts b/src/interfaces/extension.ts index 24da5b93..836c994d 100644 --- a/src/interfaces/extension.ts +++ b/src/interfaces/extension.ts @@ -252,10 +252,7 @@ class Extension { public static CheckCitizen(value: string) { let citizen = value; if (citizen == null || citizen == "" || citizen == undefined) { - throw new HttpError( - HttpStatus.NOT_FOUND, - "กรุณากรอกข้อมูลรหัสบัตรประจำตัวประชาชน", - ); + throw new HttpError(HttpStatus.NOT_FOUND, "กรุณากรอกข้อมูลรหัสบัตรประจำตัวประชาชน"); } if (citizen.length !== 13) { throw new HttpError( @@ -281,10 +278,7 @@ class Extension { const chkDigit = (11 - calStp2) % 10; if (citizenIdDigits[12] !== chkDigit) { - throw new HttpError( - HttpStatus.NOT_FOUND, - "ข้อมูลรหัสบัตรประจำตัวประชาชนไม่ถูกต้อง" - ); + throw new HttpError(HttpStatus.NOT_FOUND, "ข้อมูลรหัสบัตรประจำตัวประชาชนไม่ถูกต้อง"); } return citizen; } @@ -295,6 +289,8 @@ class Extension { subtractYear: number = 0, ): number { if (appointDate == null || appointDate == undefined) return 0; + // Check for Invalid Date + if (isNaN(appointDate.getTime())) return 0; const now = new Date(); if (now.getMonth() - appointDate.getMonth() >= 6) { return now.getFullYear() - appointDate.getFullYear() + 1 + plusYear - subtractYear; @@ -312,13 +308,20 @@ class Extension { return years; } - public static CalculateAgeStrV2(date: Date, plusYear: number = 0, subtractYear: number = 0, method?:string) { + public static CalculateAgeStrV2( + date: Date, + plusYear: number = 0, + subtractYear: number = 0, + method?: string, + ) { if (date == null || date == undefined) return ""; + // Check for Invalid Date + if (isNaN(date.getTime())) return ""; const currentDate = new Date(); if (date > currentDate && method !== "GET") { throw new Error("วันเกิดต้องไม่มากกว่าวันที่ปัจจุบัน"); - }else if(date > currentDate && method === "GET"){ - return "" + } else if (date > currentDate && method === "GET") { + return ""; } let years = currentDate.getFullYear() - date.getFullYear(); @@ -388,9 +391,13 @@ class Extension { } public static toDateOnlyString(date: Date): string { - return date.getFullYear() + "-" + - String(date.getMonth() + 1).padStart(2, "0") + "-" + - String(date.getDate()).padStart(2, "0"); + return ( + date.getFullYear() + + "-" + + String(date.getMonth() + 1).padStart(2, "0") + + "-" + + String(date.getDate()).padStart(2, "0") + ); } } From 0a3deb429362b3c874879b5628f56660a2bd0ee8 Mon Sep 17 00:00:00 2001 From: Adisak Date: Fri, 13 Feb 2026 16:56:56 +0700 Subject: [PATCH 089/178] fix --- src/controllers/CommandController.ts | 382 +++++++++++++-------------- 1 file changed, 191 insertions(+), 191 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 90ea9a38..b948289e 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -297,7 +297,7 @@ export class CommandController extends Controller { status == null || status == undefined || status == "" ? null : status.trim().toLocaleUpperCase() == "NEW" || - status.trim().toLocaleUpperCase() == "DRAFT" + status.trim().toLocaleUpperCase() == "DRAFT" ? ["NEW", "DRAFT"] : [status.trim().toLocaleUpperCase()], }, @@ -419,7 +419,7 @@ export class CommandController extends Controller { orgRevision: true, orgRoot: true, orgChild1: true, - orgChild2: true, + orgChild2: true, orgChild3: true, orgChild4: true, }, @@ -431,20 +431,20 @@ export class CommandController extends Controller { x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); - + const posNo = - currentHolder != null && currentHolder.orgChild4 != null - ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild3 != null - ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild2 != null - ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild1 != null - ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder?.orgRoot != null - ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` - : null; - + currentHolder != null && currentHolder.orgChild4 != null + ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild3 != null + ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild2 != null + ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild1 != null + ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder?.orgRoot != null + ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` + : null; + const position = await this.positionRepository.findOne({ where: { positionIsSelected: true, @@ -751,7 +751,7 @@ export class CommandController extends Controller { @Request() request: RequestWithUser, ) { await new permission().PermissionUpdate(request, "COMMAND"); - + if (!Array.isArray(requestBody)) { throw new HttpError(HttpStatusCode.BAD_REQUEST, "รูปแบบข้อมูลไม่ถูกต้อง"); } @@ -801,8 +801,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -845,8 +845,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -889,8 +889,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -1174,8 +1174,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); await this.commandReciveRepository.delete({ commandId: command.id }); command.status = "CANCEL"; @@ -1240,8 +1240,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); await this.commandSendCCRepository.delete({ commandSendId: In(commandSend.map((x) => x.id)) }); await this.commandReciveRepository.delete({ commandId: command.id }); @@ -1393,11 +1393,11 @@ export class CommandController extends Controller { let profiles = command && command.commandRecives.length > 0 ? command.commandRecives - .filter((x) => x.profileId != null) - .map((x) => ({ - receiverUserId: x.profileId, - notiLink: "", - })) + .filter((x) => x.profileId != null) + .map((x) => ({ + receiverUserId: x.profileId, + notiLink: "", + })) : []; const msgNoti = { @@ -1429,8 +1429,8 @@ export class CommandController extends Controller { refIds: command.commandRecives.filter((x) => x.refId != null).map((x) => x.refId), status: "WAITING", }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); await this.commandRepository.save(command); } else { const path = commandTypePath(command.commandType.code); @@ -1567,7 +1567,7 @@ export class CommandController extends Controller { ); await this.profileRepository.save(profiles); } - } catch {} + } catch { } type = "EMPLOYEE"; try { @@ -1599,7 +1599,7 @@ export class CommandController extends Controller { ); await this.profileEmployeeRepository.save(profiles); } - } catch {} + } catch { } return new HttpSuccess(); } @@ -1663,7 +1663,7 @@ export class CommandController extends Controller { }), ); } - } catch {} + } catch { } type = "EMPLOYEE"; try { @@ -1718,7 +1718,7 @@ export class CommandController extends Controller { }), ); } - } catch {} + } catch { } return new HttpSuccess(); } @@ -1931,7 +1931,7 @@ export class CommandController extends Controller { .then((x) => { res = x; }) - .catch((x) => {}); + .catch((x) => { }); } let _command; @@ -2009,76 +2009,76 @@ export class CommandController extends Controller { profile?.current_holders.length == 0 ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild4 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4.orgChild4ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild3 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3.orgChild3ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2 != null + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild2 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2.orgChild2ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgChild1 != null + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + )?.orgChild1 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1.orgChild1ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgRoot != null + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + )?.orgRoot != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot.orgRootShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : null; const root = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgRoot; + ?.orgRoot; const child1 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild1; + ?.orgChild1; const child2 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild2; + ?.orgChild2; const child3 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild3; + ?.orgChild3; const child4 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild4; + ?.orgChild4; let _root = root?.orgRootName; let _child1 = child1?.orgChild1Name; @@ -2139,10 +2139,10 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.isLeave == false ? (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root) + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root) : orgLeave : profileTemp.org, fullName: `${x.prefix}${x.firstName} ${x.lastName}`, @@ -2157,8 +2157,8 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.posType && profile?.posLevel ? Extension.ToThaiNumber( - `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, - ) + `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, + ) : "-" : Extension.ToThaiNumber(profileTemp.posLevel), posNo: @@ -2172,19 +2172,19 @@ export class CommandController extends Controller { ? Extension.ToThaiNumber(Extension.ToThaiShortDate_monthYear(profile?.dateRetire)) : profile?.birthDate && commandCode == "C-PM-21" ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear( - new Date( - profile.birthDate.getFullYear() + 60, - profile.birthDate.getMonth(), - profile.birthDate.getDate(), - ), + Extension.ToThaiShortDate_monthYear( + new Date( + profile.birthDate.getFullYear() + 60, + profile.birthDate.getMonth(), + profile.birthDate.getDate(), ), - ) + ), + ) : "-", dateExecute: command.commandExcecuteDate ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), - ) + Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), + ) : "-", remark: x.remarkVertical ? x.remarkVertical : "-", }; @@ -2285,7 +2285,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => {}); + .catch(() => { }); let issue = command.isBangkok == "OFFICE" @@ -2342,13 +2342,13 @@ export class CommandController extends Controller { : Extension.ToThaiNumber(Extension.ToThaiFullDate2(command.commandExcecuteDate)), operators: operators.length > 0 ? operators.map(x => ({ - fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, - roleName: x.roleName - })) + fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, + roleName: x.roleName + })) : [{ - fullName: "", - roleName: "เจ้าหน้าที่ดำเนินการ" - }] + fullName: "", + roleName: "เจ้าหน้าที่ดำเนินการ" + }] }, }); } @@ -2585,7 +2585,7 @@ export class CommandController extends Controller { orgRevision: true, orgRoot: true, orgChild1: true, - orgChild2: true, + orgChild2: true, orgChild3: true, orgChild4: true, }, @@ -2597,20 +2597,20 @@ export class CommandController extends Controller { x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); - + const posNo = - currentHolder != null && currentHolder.orgChild4 != null - ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild3 != null - ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild2 != null - ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild1 != null - ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder?.orgRoot != null - ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` - : null; - + currentHolder != null && currentHolder.orgChild4 != null + ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild3 != null + ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild2 != null + ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild1 != null + ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder?.orgRoot != null + ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` + : null; + const position = await this.positionRepository.findOne({ where: { positionIsSelected: true, @@ -2656,8 +2656,8 @@ export class CommandController extends Controller { refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), status: "REPORT", }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); let order = command.commandRecives == null || command.commandRecives.length <= 0 ? 0 @@ -3430,27 +3430,27 @@ export class CommandController extends Controller { ? x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName : x.orgChild3 == null ? x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName + : x.orgChild4 == null + ? x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + "\n" + x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName - : x.orgChild4 == null - ? x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName : x.orgChild4.orgChild4Name + - "\n" + - x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName, + "\n" + + x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName, positionName: x?.current_holder.position ?? _null, profileId: x?.current_holder.id ?? _null, }); @@ -3978,7 +3978,7 @@ export class CommandController extends Controller { // relations: ["roleKeycloaks"], relations: { roleKeycloaks: true, - posType: true , + posType: true, posLevel: true } }); @@ -4048,18 +4048,18 @@ export class CommandController extends Controller { 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; + 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:{ + relations: { orgRoot: true, orgChild1: true, orgChild2: true, @@ -4243,7 +4243,7 @@ export class CommandController extends Controller { profile.isActive = true; } await this.profileRepository.save(profile); - // Task #2190 + // Task #2190 if (code && ["C-PM-17", "C-PM-18"].includes(code)) { let organizeName = ""; if (orgRootRef) { @@ -4377,7 +4377,7 @@ export class CommandController extends Controller { // relations: ["roleKeycloaks"], relations: { roleKeycloaks: true, - posType: true , + posType: true, posLevel: true } }); @@ -4448,18 +4448,18 @@ export class CommandController extends Controller { 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; + 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:{ + relations: { orgRoot: true, orgChild1: true, orgChild2: true, @@ -4494,7 +4494,7 @@ export class CommandController extends Controller { // profile.posLevelId = _null; } await this.profileEmployeeRepository.save(profile); - // Task #2190 + // Task #2190 if (code && ["C-PM-23", "C-PM-43"].includes(code)) { let organizeName = ""; if (orgRootRef) { @@ -5024,14 +5024,14 @@ export class CommandController extends Controller { orgRevisionIsDraft: false, }, }); - let orgRootRef = null; + let orgRootRef = null; let orgChild1Ref = null; let orgChild2Ref = null; let orgChild3Ref = null; let orgChild4Ref = null; let profile; - let isEmployee:boolean = false; - let retireTypeName:string = ""; + let isEmployee: boolean = false; + let retireTypeName: string = ""; // ขรก. if (item.profileType && item.profileType.trim().toUpperCase() == "OFFICER") { profile = await this.profileRepository.findOne({ @@ -5073,11 +5073,11 @@ export class CommandController extends Controller { const orgRevisionRef = profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - orgRootRef = orgRevisionRef?.orgRoot ?? null; - orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + orgRootRef = orgRevisionRef?.orgRoot ?? null; + orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; let position = profile.current_holders @@ -5216,7 +5216,7 @@ export class CommandController extends Controller { } await this.profileRepository.save(_profile); } - } + } // ลูกจ้าง else { isEmployee = true; @@ -5251,12 +5251,12 @@ export class CommandController extends Controller { const nextOrder = lastSalary ? lastSalary.order + 1 : 1; const orgRevisionRef = profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - orgRootRef = orgRevisionRef?.orgRoot ?? null; - orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; - + orgRootRef = orgRevisionRef?.orgRoot ?? null; + orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + // ประวัติตำแหน่ง const data = new ProfileSalary(); data.posNumCodeSit = _posNumCodeSit; @@ -5407,9 +5407,9 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - let _posLevelName: string = !isEmployee - ? `${profile.posLevel?.posLevelName}` - : `${profile.posType?.posTypeName} ${profile.posLevel?.posLevelName}`; + let _posLevelName: string = !isEmployee + ? `${profile.posLevel?.posLevelName}` + : `${profile.posType?.posTypeName} ${profile.posLevel?.posLevelName}`; await PostRetireToExprofile( // profile.citizenId ?? "", // profile.prefix ?? "", @@ -5814,8 +5814,8 @@ export class CommandController extends Controller { } } const leaveType = await this.leaveType.findOne({ - select:{ id: true, limit: true, code: true }, - where:{ code: "LV-005" } + select: { id: true, limit: true, code: true }, + where: { code: "LV-005" } }); await Promise.all( body.data.map(async (item) => { @@ -5865,26 +5865,26 @@ export class CommandController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild4 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; @@ -6118,26 +6118,26 @@ export class CommandController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild4 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; @@ -6899,7 +6899,7 @@ export class CommandController extends Controller { profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; profile.position = positionNew.positionName; - profile.dateStart = new Date(); + // profile.dateStart = new Date(); await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); await this.positionRepository.save(positionNew, { data: req }); @@ -6979,8 +6979,8 @@ export class CommandController extends Controller { prefix: avatar, fileName: fileName, }) - .then(() => {}) - .catch(() => {}); + .then(() => { }) + .catch(() => { }); } } }), @@ -8091,7 +8091,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => {}); + .catch(() => { }); let issue = command.isBangkok == "OFFICE" From 1a9947d36279c72391c3a8733295a4ddc39bb947 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 13 Feb 2026 17:03:43 +0700 Subject: [PATCH 090/178] fix error Invalid time value --- src/interfaces/extension.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/interfaces/extension.ts b/src/interfaces/extension.ts index 836c994d..ff6aeccb 100644 --- a/src/interfaces/extension.ts +++ b/src/interfaces/extension.ts @@ -300,6 +300,9 @@ class Extension { } public static CalculateAge(appointDate: Date, plusYear: number = 0, subtractYear: number = 0) { + if (appointDate == null || appointDate == undefined) return 0; + // Check for Invalid Date + if (isNaN(appointDate.getTime())) return 0; let currentDate = new Date().getTime(); let appointDateTime = new Date(appointDate).getTime(); let ageInMilliseconds = currentDate - appointDateTime; From 17f7fc5d845497b52c2e8a666b1ddb66649803e8 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 13 Feb 2026 17:45:44 +0700 Subject: [PATCH 091/178] fix registry employer Invalid time value --- src/interfaces/date-serializer.ts | 5 ++++- src/interfaces/extension.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/interfaces/date-serializer.ts b/src/interfaces/date-serializer.ts index 5c876ffc..b3f0c541 100644 --- a/src/interfaces/date-serializer.ts +++ b/src/interfaces/date-serializer.ts @@ -14,7 +14,10 @@ export class DateSerializer { static setupDateSerialization() { // Override Date.prototype.toJSON to use local time - Date.prototype.toJSON = function () { + Date.prototype.toJSON = function (): any { + // Check for Invalid Date - return null for invalid dates + if (isNaN(this.getTime())) return null; + const offset = 7 * 60; // Thailand timezone offset in minutes const localTime = new Date(this.getTime() + offset * 60 * 1000); const isoString = localTime.toISOString(); diff --git a/src/interfaces/extension.ts b/src/interfaces/extension.ts index ff6aeccb..405ed849 100644 --- a/src/interfaces/extension.ts +++ b/src/interfaces/extension.ts @@ -184,6 +184,7 @@ class Extension { } public static ToThaiFullDate(value: Date) { + if (value == null || value == undefined || isNaN(value.getTime())) return ""; let yy = value.getFullYear() < 2400 ? value.getFullYear() + 543 : value.getFullYear(); return ( "วันที่ " + @@ -195,11 +196,13 @@ class Extension { ); } public static ToThaiFullDate2(value: Date) { + if (value == null || value == undefined || isNaN(value.getTime())) return ""; let yy = value.getFullYear() < 2400 ? value.getFullYear() + 543 : value.getFullYear(); return value.getDate() + " " + Extension.ToThaiMonth(value.getMonth() + 1) + " " + yy; } public static ToThaiShortDate(value: Date) { + if (value == null || value == undefined || isNaN(value.getTime())) return ""; let yy = value.getFullYear() < 2400 ? value.getFullYear() + 543 : value.getFullYear(); return ( "วันที่ " + @@ -212,6 +215,7 @@ class Extension { } public static ToThaiShortDate_noPrefix(value: Date) { + if (value == null || value == undefined || isNaN(value.getTime())) return ""; let yy = value.getFullYear() < 2400 ? value.getFullYear() + 543 : value.getFullYear(); return ( value.getDate() + @@ -223,6 +227,7 @@ class Extension { } public static ToThaiShortDate_monthYear(value: Date) { + if (value == null || value == undefined || isNaN(value.getTime())) return ""; let yy = value.getFullYear() < 2400 ? value.getFullYear() + 543 : value.getFullYear(); return ( value.getDate() + " เดือน " + Extension.ToThaiMonth(value.getMonth() + 1) + " พ.ศ. " + yy @@ -230,11 +235,13 @@ class Extension { } public static ToThaiShortDate_monthYear2(value: Date) { + if (value == null || value == undefined || isNaN(value.getTime())) return ""; let yy = value.getFullYear() < 2400 ? value.getFullYear() + 543 : value.getFullYear(); return value.getDate() + " " + Extension.ToThaiMonth(value.getMonth() + 1) + " พ.ศ. " + yy; } public static ToThaiShortYear(value: Date) { + if (value == null || value == undefined || isNaN(value.getTime())) return ""; let yy = value.getFullYear() < 2400 ? value.getFullYear() + 543 : value.getFullYear(); return yy.toString(); } @@ -394,6 +401,7 @@ class Extension { } public static toDateOnlyString(date: Date): string { + if (date == null || date == undefined || isNaN(date.getTime())) return ""; return ( date.getFullYear() + "-" + From 9382482f06368ebd3136c5387581c59c766b86f8 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 16 Feb 2026 10:30:15 +0700 Subject: [PATCH 092/178] api soft delete --- src/controllers/ProfileAbilityController.ts | 39 ++++++++++++++++++ .../ProfileAbilityEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileAbilityEmployeeTempController.ts | 39 ++++++++++++++++++ .../ProfileAssessmentsController.ts | 39 ++++++++++++++++++ .../ProfileAssessmentsEmployeeController.ts | 39 ++++++++++++++++++ ...rofileAssessmentsEmployeeTempController.ts | 39 ++++++++++++++++++ .../ProfileAssistanceController.ts | 39 ++++++++++++++++++ .../ProfileAssistanceEmployeeController.ts | 40 +++++++++++++++++++ ...ProfileAssistanceEmployeeTempController.ts | 39 ++++++++++++++++++ .../ProfileCertificateController.ts | 39 ++++++++++++++++++ .../ProfileCertificateEmployeeController.ts | 39 ++++++++++++++++++ ...rofileCertificateEmployeeTempController.ts | 39 ++++++++++++++++++ .../ProfileChangeNameController.ts | 39 ++++++++++++++++++ .../ProfileChangeNameEmployeeController.ts | 39 ++++++++++++++++++ ...ProfileChangeNameEmployeeTempController.ts | 39 ++++++++++++++++++ src/controllers/ProfileChildrenController.ts | 39 ++++++++++++++++++ .../ProfileChildrenEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileChildrenEmployeeTempController.ts | 39 ++++++++++++++++++ .../ProfileDisciplineController.ts | 39 ++++++++++++++++++ .../ProfileDisciplineEmployeeController.ts | 39 ++++++++++++++++++ ...ProfileDisciplineEmployeeTempController.ts | 39 ++++++++++++++++++ src/controllers/ProfileDutyController.ts | 39 ++++++++++++++++++ .../ProfileDutyEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileDutyEmployeeTempController.ts | 39 ++++++++++++++++++ .../ProfileEducationsController.ts | 39 ++++++++++++++++++ .../ProfileEducationsEmployeeController.ts | 39 ++++++++++++++++++ ...ProfileEducationsEmployeeTempController.ts | 39 ++++++++++++++++++ src/controllers/ProfileHonorController.ts | 39 ++++++++++++++++++ .../ProfileHonorEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileHonorEmployeeTempController.ts | 39 ++++++++++++++++++ src/controllers/ProfileInsigniaController.ts | 39 ++++++++++++++++++ .../ProfileInsigniaEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileInsigniaEmployeeTempController.ts | 39 ++++++++++++++++++ src/controllers/ProfileLeaveController.ts | 39 ++++++++++++++++++ .../ProfileLeaveEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileLeaveEmployeeTempController.ts | 39 ++++++++++++++++++ src/controllers/ProfileNopaidController.ts | 39 ++++++++++++++++++ .../ProfileNopaidEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileNopaidEmployeeTempController.ts | 39 ++++++++++++++++++ src/controllers/ProfileOtherController.ts | 39 ++++++++++++++++++ .../ProfileOtherEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileOtherEmployeeTempController.ts | 39 ++++++++++++++++++ .../ProfileTrainingEmployeeController.ts | 39 ++++++++++++++++++ .../ProfileTrainingEmployeeTempController.ts | 39 ++++++++++++++++++ 44 files changed, 1717 insertions(+) diff --git a/src/controllers/ProfileAbilityController.ts b/src/controllers/ProfileAbilityController.ts index c8012057..3faa7c08 100644 --- a/src/controllers/ProfileAbilityController.ts +++ b/src/controllers/ProfileAbilityController.ts @@ -174,6 +174,45 @@ export class ProfileAbilityController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลความสามารถพิเศษ + * @summary API ลบข้อมูลความสามารถพิเศษ + * @param abilityId คีย์ความสามารถพิเศษ + */ + @Patch("update-delete/{abilityId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() abilityId: string, + ) { + const record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileAbilityHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAbilityId = abilityId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAbilityRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAbilityHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{abilityId}") public async deleteProfileAbility(@Path() abilityId: string, @Request() req: RequestWithUser) { const _record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); diff --git a/src/controllers/ProfileAbilityEmployeeController.ts b/src/controllers/ProfileAbilityEmployeeController.ts index d7ba3ae9..096b77d8 100644 --- a/src/controllers/ProfileAbilityEmployeeController.ts +++ b/src/controllers/ProfileAbilityEmployeeController.ts @@ -183,6 +183,45 @@ export class ProfileAbilityEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลความสามารถพิเศษ + * @summary API ลบข้อมูลความสามารถพิเศษ + * @param abilityId คีย์ความสามารถพิเศษ + */ + @Patch("update-delete/{abilityId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() abilityId: string, + ) { + const record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileAbilityHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAbilityId = abilityId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAbilityRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAbilityHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{abilityId}") public async deleteProfileAbility(@Path() abilityId: string, @Request() req: RequestWithUser) { const _record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); diff --git a/src/controllers/ProfileAbilityEmployeeTempController.ts b/src/controllers/ProfileAbilityEmployeeTempController.ts index 624c8ded..3a4e356a 100644 --- a/src/controllers/ProfileAbilityEmployeeTempController.ts +++ b/src/controllers/ProfileAbilityEmployeeTempController.ts @@ -173,6 +173,45 @@ export class ProfileAbilityEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลความสามารถพิเศษ + * @summary API ลบข้อมูลความสามารถพิเศษ + * @param abilityId คีย์ความสามารถพิเศษ + */ + @Patch("update-delete/{abilityId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() abilityId: string, + ) { + const record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileAbilityHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAbilityId = abilityId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAbilityRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAbilityHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{abilityId}") public async deleteProfileAbility(@Path() abilityId: string, @Request() req: RequestWithUser) { const _record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); diff --git a/src/controllers/ProfileAssessmentsController.ts b/src/controllers/ProfileAssessmentsController.ts index 53906cb2..1a7bc662 100644 --- a/src/controllers/ProfileAssessmentsController.ts +++ b/src/controllers/ProfileAssessmentsController.ts @@ -186,6 +186,45 @@ export class ProfileAssessmentsController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลผลการประเมินการปฏิบัติราชการ + * @summary API ลบข้อมูลผลการประเมินการปฏิบัติราชการ + * @param assessmentId คีย์ผลการประเมินการปฏิบัติราชการ + */ + @Patch("update-delete/{assessmentId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() assessmentId: string, + ) { + const record = await this.profileAssessmentsRepository.findOneBy({ id: assessmentId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileAssessmentHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAssessmentId = assessmentId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAssessmentsRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAssessmentsHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{assessmentId}") public async deleteProfileAssessment( @Path() assessmentId: string, diff --git a/src/controllers/ProfileAssessmentsEmployeeController.ts b/src/controllers/ProfileAssessmentsEmployeeController.ts index 0f7f3d81..04587937 100644 --- a/src/controllers/ProfileAssessmentsEmployeeController.ts +++ b/src/controllers/ProfileAssessmentsEmployeeController.ts @@ -193,6 +193,45 @@ export class ProfileAssessmentsEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลผลการประเมินการปฏิบัติราชการ + * @summary API ลบข้อมูลผลการประเมินการปฏิบัติราชการ + * @param assessmentId คีย์ผลการประเมินการปฏิบัติราชการ + */ + @Patch("update-delete/{assessmentId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() assessmentId: string, + ) { + const record = await this.profileAssessmentsRepository.findOneBy({ id: assessmentId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileAssessmentHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAssessmentId = assessmentId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAssessmentsRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAssessmentsHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{assessmentId}") public async deleteProfileAssessment( @Path() assessmentId: string, diff --git a/src/controllers/ProfileAssessmentsEmployeeTempController.ts b/src/controllers/ProfileAssessmentsEmployeeTempController.ts index b6bc45e6..77689709 100644 --- a/src/controllers/ProfileAssessmentsEmployeeTempController.ts +++ b/src/controllers/ProfileAssessmentsEmployeeTempController.ts @@ -181,6 +181,45 @@ export class ProfileAssessmentsEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลผลการประเมินการปฏิบัติราชการ + * @summary API ลบข้อมูลผลการประเมินการปฏิบัติราชการ + * @param assessmentId คีย์ผลการประเมินการปฏิบัติราชการ + */ + @Patch("update-delete/{assessmentId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() assessmentId: string, + ) { + const record = await this.profileAssessmentsRepository.findOneBy({ id: assessmentId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileAssessmentHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAssessmentId = assessmentId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAssessmentsRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAssessmentsHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{assessmentId}") public async deleteProfileAssessment( @Path() assessmentId: string, diff --git a/src/controllers/ProfileAssistanceController.ts b/src/controllers/ProfileAssistanceController.ts index 3690f5e6..a2ec5643 100644 --- a/src/controllers/ProfileAssistanceController.ts +++ b/src/controllers/ProfileAssistanceController.ts @@ -175,6 +175,45 @@ export class ProfileAssistanceController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลช่วยราชการ + * @summary API ลบข้อมูลช่วยราชการ + * @param assistanceId คีย์ช่วยราชการ + */ + @Patch("update-delete/{assistanceId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() assistanceId: string, + ) { + const record = await this.profileAssistanceRepo.findOneBy({ id: assistanceId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileAssistanceHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAssistanceId = assistanceId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAssistanceRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAssistanceHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{assistanceId}") public async deleteProfileAssistance( @Path() assistanceId: string, diff --git a/src/controllers/ProfileAssistanceEmployeeController.ts b/src/controllers/ProfileAssistanceEmployeeController.ts index 88617322..065b5142 100644 --- a/src/controllers/ProfileAssistanceEmployeeController.ts +++ b/src/controllers/ProfileAssistanceEmployeeController.ts @@ -183,6 +183,46 @@ export class ProfileAssistanceEmployeeController extends Controller { return new HttpSuccess(); } + + /** + * API ลบข้อมูลช่วยราชการ + * @summary API ลบข้อมูลช่วยราชการ + * @param assistanceId คีย์ช่วยราชการ + */ + @Patch("update-delete/{assistanceId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() assistanceId: string, + ) { + const record = await this.profileAssistanceRepo.findOneBy({ id: assistanceId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileAssistanceHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAssistanceId = assistanceId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAssistanceRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAssistanceHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{assistanceId}") public async deleteProfileAssistance( @Path() assistanceId: string, diff --git a/src/controllers/ProfileAssistanceEmployeeTempController.ts b/src/controllers/ProfileAssistanceEmployeeTempController.ts index da37c3a5..26d88476 100644 --- a/src/controllers/ProfileAssistanceEmployeeTempController.ts +++ b/src/controllers/ProfileAssistanceEmployeeTempController.ts @@ -173,6 +173,45 @@ export class ProfileAssistanceEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลช่วยราชการ + * @summary API ลบข้อมูลช่วยราชการ + * @param assistanceId คีย์ช่วยราชการ + */ + @Patch("update-delete/{assistanceId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() assistanceId: string, + ) { + const record = await this.profileAssistanceRepo.findOneBy({ id: assistanceId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileAssistanceHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileAssistanceId = assistanceId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileAssistanceRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileAssistanceHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{assistanceId}") public async deleteProfileAssistance( @Path() assistanceId: string, diff --git a/src/controllers/ProfileCertificateController.ts b/src/controllers/ProfileCertificateController.ts index 5cbc019a..9d29dc10 100644 --- a/src/controllers/ProfileCertificateController.ts +++ b/src/controllers/ProfileCertificateController.ts @@ -166,6 +166,45 @@ export class ProfileCertificateController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลใบอนุญาตประกอบวิชาชีพ + * @summary API ลบข้อมูลใบอนุญาตประกอบวิชาชีพ + * @param certificateId คีย์ใบอนุญาตประกอบวิชาชีพ + */ + @Patch("update-delete/{certificateId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() certificateId: string, + ) { + const record = await this.certificateRepo.findOneBy({ id: certificateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileCertificateHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileCertificateId = certificateId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.certificateRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.certificateHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{certificateId}") public async deleteCertificate(@Path() certificateId: string, @Request() req: RequestWithUser) { const _record = await this.certificateRepo.findOneBy({ id: certificateId }); diff --git a/src/controllers/ProfileCertificateEmployeeController.ts b/src/controllers/ProfileCertificateEmployeeController.ts index 6bb4c1df..74796f9e 100644 --- a/src/controllers/ProfileCertificateEmployeeController.ts +++ b/src/controllers/ProfileCertificateEmployeeController.ts @@ -174,6 +174,45 @@ export class ProfileCertificateEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลใบอนุญาตประกอบวิชาชีพ + * @summary API ลบข้อมูลใบอนุญาตประกอบวิชาชีพ + * @param certificateId คีย์ใบอนุญาตประกอบวิชาชีพ + */ + @Patch("update-delete/{certificateId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() certificateId: string, + ) { + const record = await this.certificateRepo.findOneBy({ id: certificateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileCertificateHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileCertificateId = certificateId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.certificateRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.certificateHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{certificateId}") public async deleteCertificate(@Path() certificateId: string, @Request() req: RequestWithUser) { const _record = await this.certificateRepo.findOneBy({ id: certificateId }); diff --git a/src/controllers/ProfileCertificateEmployeeTempController.ts b/src/controllers/ProfileCertificateEmployeeTempController.ts index f5619023..8ad0fa3f 100644 --- a/src/controllers/ProfileCertificateEmployeeTempController.ts +++ b/src/controllers/ProfileCertificateEmployeeTempController.ts @@ -162,6 +162,45 @@ export class ProfileCertificateEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลใบอนุญาตประกอบวิชาชีพ + * @summary API ลบข้อมูลใบอนุญาตประกอบวิชาชีพ + * @param certificateId คีย์ใบอนุญาตประกอบวิชาชีพ + */ + @Patch("update-delete/{certificateId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() certificateId: string, + ) { + const record = await this.certificateRepo.findOneBy({ id: certificateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileCertificateHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileCertificateId = certificateId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.certificateRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.certificateHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{certificateId}") public async deleteCertificate(@Path() certificateId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 5b772f12..26741c46 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -191,6 +191,45 @@ export class ProfileChangeNameController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประวัติการเปลี่ยนชื่อ - นามสกุล + * @summary API ลบข้อมูลประวัติการเปลี่ยนชื่อ - นามสกุล + * @param trainingId คีย์ประวัติการเปลี่ยนชื่อ - นามสกุล + */ + @Patch("update-delete/{changeNameId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() changeNameId: string, + ) { + const record = await this.changeNameRepository.findOneBy({ id: changeNameId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileChangeNameHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileChangeNameId = changeNameId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.changeNameRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.changeNameHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{changeNameId}") public async deleteTraning(@Path() changeNameId: string, @Request() req: RequestWithUser) { const _record = await this.changeNameRepository.findOneBy({ id: changeNameId }); diff --git a/src/controllers/ProfileChangeNameEmployeeController.ts b/src/controllers/ProfileChangeNameEmployeeController.ts index 547368e1..7772646d 100644 --- a/src/controllers/ProfileChangeNameEmployeeController.ts +++ b/src/controllers/ProfileChangeNameEmployeeController.ts @@ -189,6 +189,45 @@ export class ProfileChangeNameEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประวัติการเปลี่ยนชื่อ - นามสกุล + * @summary API ลบข้อมูลประวัติการเปลี่ยนชื่อ - นามสกุล + * @param trainingId คีย์ประวัติการเปลี่ยนชื่อ - นามสกุล + */ + @Patch("update-delete/{changeNameId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() changeNameId: string, + ) { + const record = await this.changeNameRepository.findOneBy({ id: changeNameId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileChangeNameHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileChangeNameId = changeNameId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.changeNameRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.changeNameHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{changeNameId}") public async deleteTraning(@Path() changeNameId: string, @Request() req: RequestWithUser) { const _record = await this.changeNameRepository.findOneBy({ id: changeNameId }); diff --git a/src/controllers/ProfileChangeNameEmployeeTempController.ts b/src/controllers/ProfileChangeNameEmployeeTempController.ts index f7a141f7..049e2b07 100644 --- a/src/controllers/ProfileChangeNameEmployeeTempController.ts +++ b/src/controllers/ProfileChangeNameEmployeeTempController.ts @@ -181,6 +181,45 @@ export class ProfileChangeNameEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประวัติการเปลี่ยนชื่อ - นามสกุล + * @summary API ลบข้อมูลประวัติการเปลี่ยนชื่อ - นามสกุล + * @param trainingId คีย์ประวัติการเปลี่ยนชื่อ - นามสกุล + */ + @Patch("update-delete/{changeNameId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() changeNameId: string, + ) { + const record = await this.changeNameRepository.findOneBy({ id: changeNameId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileChangeNameHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileChangeNameId = changeNameId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.changeNameRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.changeNameHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{changeNameId}") public async deleteTraning(@Path() changeNameId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileChildrenController.ts b/src/controllers/ProfileChildrenController.ts index 577b6781..98319cf0 100644 --- a/src/controllers/ProfileChildrenController.ts +++ b/src/controllers/ProfileChildrenController.ts @@ -143,6 +143,45 @@ export class ProfileChildrenController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลบุตร + * @summary API ลบข้อมูลบุตร + * @param trainingId คีย์บุตร + */ + @Patch("update-delete/{childrenId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() childrenId: string, + ) { + const record = await this.childrenRepository.findOneBy({ id: childrenId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileChildrenHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileChildrenId = childrenId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.childrenRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.childrenHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{childrenId}") public async deleteTraning(@Path() childrenId: string, @Request() req: RequestWithUser) { const _record = await this.childrenRepository.findOneBy({ id: childrenId }); diff --git a/src/controllers/ProfileChildrenEmployeeController.ts b/src/controllers/ProfileChildrenEmployeeController.ts index 7a0cd772..2be404ef 100644 --- a/src/controllers/ProfileChildrenEmployeeController.ts +++ b/src/controllers/ProfileChildrenEmployeeController.ts @@ -156,6 +156,45 @@ export class ProfileChildrenEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลบุตร + * @summary API ลบข้อมูลบุตร + * @param trainingId คีย์บุตร + */ + @Patch("update-delete/{childrenId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() childrenId: string, + ) { + const record = await this.childrenRepository.findOneBy({ id: childrenId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileChildrenHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileChildrenId = childrenId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.childrenRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.childrenHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{childrenId}") public async deleteTraning(@Path() childrenId: string, @Request() req: RequestWithUser) { const _record = await this.childrenRepository.findOneBy({ id: childrenId }); diff --git a/src/controllers/ProfileChildrenEmployeeTempController.ts b/src/controllers/ProfileChildrenEmployeeTempController.ts index 2134e954..83a04a30 100644 --- a/src/controllers/ProfileChildrenEmployeeTempController.ts +++ b/src/controllers/ProfileChildrenEmployeeTempController.ts @@ -143,6 +143,45 @@ export class ProfileChildrenEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลบุตร + * @summary API ลบข้อมูลบุตร + * @param trainingId คีย์บุตร + */ + @Patch("update-delete/{childrenId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() childrenId: string, + ) { + const record = await this.childrenRepository.findOneBy({ id: childrenId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileChildrenHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileChildrenId = childrenId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.childrenRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.childrenHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{childrenId}") public async deleteTraning(@Path() childrenId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileDisciplineController.ts b/src/controllers/ProfileDisciplineController.ts index e7ec5a94..17cf3689 100644 --- a/src/controllers/ProfileDisciplineController.ts +++ b/src/controllers/ProfileDisciplineController.ts @@ -175,6 +175,45 @@ export class ProfileDisciplineController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลวินัย + * @summary API ลบข้อมูลวินัย + * @param disciplineId คีย์วินัย + */ + @Patch("update-delete/{disciplineId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() disciplineId: string, + ) { + const record = await this.disciplineRepository.findOneBy({ id: disciplineId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileDisciplineHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileDisciplineId = disciplineId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.disciplineRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.disciplineHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{disciplineId}") public async deleteDiscipline(@Path() disciplineId: string, @Request() req: RequestWithUser) { const _record = await this.disciplineRepository.findOneBy({ id: disciplineId }); diff --git a/src/controllers/ProfileDisciplineEmployeeController.ts b/src/controllers/ProfileDisciplineEmployeeController.ts index 22dd3f24..44460d44 100644 --- a/src/controllers/ProfileDisciplineEmployeeController.ts +++ b/src/controllers/ProfileDisciplineEmployeeController.ts @@ -179,6 +179,45 @@ export class ProfileDisciplineEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลวินัย + * @summary API ลบข้อมูลวินัย + * @param disciplineId คีย์วินัย + */ + @Patch("update-delete/{disciplineId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() disciplineId: string, + ) { + const record = await this.disciplineRepository.findOneBy({ id: disciplineId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileDisciplineHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileDisciplineId = disciplineId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.disciplineRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.disciplineHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{disciplineId}") public async deleteDiscipline(@Path() disciplineId: string, @Request() req: RequestWithUser) { const _record = await this.disciplineRepository.findOneBy({ id: disciplineId }); diff --git a/src/controllers/ProfileDisciplineEmployeeTempController.ts b/src/controllers/ProfileDisciplineEmployeeTempController.ts index b1100ab2..7699b8ac 100644 --- a/src/controllers/ProfileDisciplineEmployeeTempController.ts +++ b/src/controllers/ProfileDisciplineEmployeeTempController.ts @@ -169,6 +169,45 @@ export class ProfileDisciplineEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลวินัย + * @summary API ลบข้อมูลวินัย + * @param disciplineId คีย์วินัย + */ + @Patch("update-delete/{disciplineId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() disciplineId: string, + ) { + const record = await this.disciplineRepository.findOneBy({ id: disciplineId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileDisciplineHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileDisciplineId = disciplineId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.disciplineRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.disciplineHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{disciplineId}") public async deleteDiscipline(@Path() disciplineId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileDutyController.ts b/src/controllers/ProfileDutyController.ts index 43161383..15bcf53e 100644 --- a/src/controllers/ProfileDutyController.ts +++ b/src/controllers/ProfileDutyController.ts @@ -150,6 +150,45 @@ export class ProfileDutyController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลปฏิบัติราชการพิเศษ + * @summary API ลบข้อมูลปฏิบัติราชการพิเศษ + * @param dutyId คีย์ปฏิบัติราชการพิเศษ + */ + @Patch("update-delete/{dutyId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() dutyId: string, + ) { + const record = await this.dutyRepository.findOneBy({ id: dutyId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileDutyHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileDutyId = dutyId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.dutyRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.dutyHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{dutyId}") public async deleteDuty(@Path() dutyId: string, @Request() req: RequestWithUser) { const _record = await this.dutyRepository.findOneBy({ id: dutyId }); diff --git a/src/controllers/ProfileDutyEmployeeController.ts b/src/controllers/ProfileDutyEmployeeController.ts index 82a3e9b4..278cd736 100644 --- a/src/controllers/ProfileDutyEmployeeController.ts +++ b/src/controllers/ProfileDutyEmployeeController.ts @@ -159,6 +159,45 @@ export class ProfileDutyEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลปฏิบัติราชการพิเศษ + * @summary API ลบข้อมูลปฏิบัติราชการพิเศษ + * @param dutyId คีย์ปฏิบัติราชการพิเศษ + */ + @Patch("update-delete/{dutyId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() dutyId: string, + ) { + const record = await this.dutyRepository.findOneBy({ id: dutyId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileDutyHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileDutyId = dutyId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.dutyRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.dutyHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{dutyId}") public async deleteDuty(@Path() dutyId: string, @Request() req: RequestWithUser) { const _record = await this.dutyRepository.findOneBy({ id: dutyId }); diff --git a/src/controllers/ProfileDutyEmployeeTempController.ts b/src/controllers/ProfileDutyEmployeeTempController.ts index 09f8f843..fa3cc605 100644 --- a/src/controllers/ProfileDutyEmployeeTempController.ts +++ b/src/controllers/ProfileDutyEmployeeTempController.ts @@ -148,6 +148,45 @@ export class ProfileDutyEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลปฏิบัติราชการพิเศษ + * @summary API ลบข้อมูลปฏิบัติราชการพิเศษ + * @param dutyId คีย์ปฏิบัติราชการพิเศษ + */ + @Patch("update-delete/{dutyId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() dutyId: string, + ) { + const record = await this.dutyRepository.findOneBy({ id: dutyId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileDutyHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileDutyId = dutyId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.dutyRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.dutyHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{dutyId}") public async deleteDuty(@Path() dutyId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileEducationsController.ts b/src/controllers/ProfileEducationsController.ts index e9013eea..8ac2d34c 100644 --- a/src/controllers/ProfileEducationsController.ts +++ b/src/controllers/ProfileEducationsController.ts @@ -208,6 +208,45 @@ export class ProfileEducationsController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประวัติการศึกษา + * @summary API ลบข้อมูลประวัติการศึกษา/ดูงาน + * @param educationId คีย์ประวัติการศึกษา + */ + @Patch("update-delete/{educationId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() educationId: string, + ) { + const record = await this.profileEducationRepo.findOneBy({ id: educationId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileEducationHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileEducationId = educationId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileEducationRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileEducationHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{educationId}") public async deleteProfileEducation( @Path() educationId: string, diff --git a/src/controllers/ProfileEducationsEmployeeController.ts b/src/controllers/ProfileEducationsEmployeeController.ts index c81a7309..0775d53a 100644 --- a/src/controllers/ProfileEducationsEmployeeController.ts +++ b/src/controllers/ProfileEducationsEmployeeController.ts @@ -220,6 +220,45 @@ export class ProfileEducationsEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประวัติการศึกษา + * @summary API ลบข้อมูลประวัติการศึกษา/ดูงาน + * @param educationId คีย์ประวัติการศึกษา + */ + @Patch("update-delete/{educationId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() educationId: string, + ) { + const record = await this.profileEducationRepo.findOneBy({ id: educationId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileEducationHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileEducationId = educationId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileEducationRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileEducationHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{educationId}") public async deleteProfileEducation( @Path() educationId: string, diff --git a/src/controllers/ProfileEducationsEmployeeTempController.ts b/src/controllers/ProfileEducationsEmployeeTempController.ts index a63c88f7..a0688591 100644 --- a/src/controllers/ProfileEducationsEmployeeTempController.ts +++ b/src/controllers/ProfileEducationsEmployeeTempController.ts @@ -210,6 +210,45 @@ export class ProfileEducationsEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประวัติการศึกษา + * @summary API ลบข้อมูลประวัติการศึกษา/ดูงาน + * @param educationId คีย์ประวัติการศึกษา + */ + @Patch("update-delete/{educationId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() educationId: string, + ) { + const record = await this.profileEducationRepo.findOneBy({ id: educationId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileEducationHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileEducationId = educationId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.profileEducationRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.profileEducationHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{educationId}") public async deleteProfileEducation( @Path() educationId: string, diff --git a/src/controllers/ProfileHonorController.ts b/src/controllers/ProfileHonorController.ts index 057f6ad0..1eb6cba4 100644 --- a/src/controllers/ProfileHonorController.ts +++ b/src/controllers/ProfileHonorController.ts @@ -176,6 +176,45 @@ export class ProfileHonorController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประกาศเกียรติคุณ + * @summary API ลบข้อมูลประกาศเกียรติคุณ + * @param honorId คีย์ประกาศเกียรติคุณ + */ + @Patch("update-delete/{honorId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() honorId: string, + ) { + const record = await this.honorRepo.findOneBy({ id: honorId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileHonorHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileHonorId = honorId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.honorRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.honorHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{honorId}") public async deleteTraning(@Path() honorId: string, @Request() req: RequestWithUser) { const _record = await this.honorRepo.findOneBy({ id: honorId }); diff --git a/src/controllers/ProfileHonorEmployeeController.ts b/src/controllers/ProfileHonorEmployeeController.ts index bacb5ca5..5ab8df44 100644 --- a/src/controllers/ProfileHonorEmployeeController.ts +++ b/src/controllers/ProfileHonorEmployeeController.ts @@ -188,6 +188,45 @@ export class ProfileHonorEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประกาศเกียรติคุณ + * @summary API ลบข้อมูลประกาศเกียรติคุณ + * @param honorId คีย์ประกาศเกียรติคุณ + */ + @Patch("update-delete/{honorId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() honorId: string, + ) { + const record = await this.honorRepo.findOneBy({ id: honorId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileHonorHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileHonorId = honorId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.honorRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.honorHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{honorId}") public async deleteTraning(@Path() honorId: string, @Request() req: RequestWithUser) { const _record = await this.honorRepo.findOneBy({ id: honorId }); diff --git a/src/controllers/ProfileHonorEmployeeTempController.ts b/src/controllers/ProfileHonorEmployeeTempController.ts index dbf1ceb3..eeba94e8 100644 --- a/src/controllers/ProfileHonorEmployeeTempController.ts +++ b/src/controllers/ProfileHonorEmployeeTempController.ts @@ -176,6 +176,45 @@ export class ProfileHonorEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลประกาศเกียรติคุณ + * @summary API ลบข้อมูลประกาศเกียรติคุณ + * @param honorId คีย์ประกาศเกียรติคุณ + */ + @Patch("update-delete/{honorId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() honorId: string, + ) { + const record = await this.honorRepo.findOneBy({ id: honorId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileHonorHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileHonorId = honorId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.honorRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.honorHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{honorId}") public async deleteTraning(@Path() honorId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileInsigniaController.ts b/src/controllers/ProfileInsigniaController.ts index af871c69..f4f5f2b4 100644 --- a/src/controllers/ProfileInsigniaController.ts +++ b/src/controllers/ProfileInsigniaController.ts @@ -199,6 +199,45 @@ export class ProfileInsigniaController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลเครื่องราชอิสริยาภรณ์ + * @summary API ลบข้อมูลเครื่องราชอิสริยาภรณ์ + * @param insigniaId คีย์เครื่องราชอิสริยาภรณ์ + */ + @Patch("update-delete/{insigniaId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() insigniaId: string, + ) { + const record = await this.insigniaRepo.findOneBy({ id: insigniaId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileInsigniaHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileInsigniaId = insigniaId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.insigniaRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.insigniaHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{insigniaId}") public async deleteInsignia(@Path() insigniaId: string, @Request() req: RequestWithUser) { const _record = await this.insigniaRepo.findOneBy({ id: insigniaId }); diff --git a/src/controllers/ProfileInsigniaEmployeeController.ts b/src/controllers/ProfileInsigniaEmployeeController.ts index e0d3ad82..7e9d11b2 100644 --- a/src/controllers/ProfileInsigniaEmployeeController.ts +++ b/src/controllers/ProfileInsigniaEmployeeController.ts @@ -207,6 +207,45 @@ export class ProfileInsigniaEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลเครื่องราชอิสริยาภรณ์ + * @summary API ลบข้อมูลเครื่องราชอิสริยาภรณ์ + * @param insigniaId คีย์เครื่องราชอิสริยาภรณ์ + */ + @Patch("update-delete/{insigniaId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() insigniaId: string, + ) { + const record = await this.insigniaRepo.findOneBy({ id: insigniaId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileInsigniaHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileInsigniaId = insigniaId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.insigniaRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.insigniaHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{insigniaId}") public async deleteInsignia(@Path() insigniaId: string, @Request() req: RequestWithUser) { const _record = await this.insigniaRepo.findOneBy({ id: insigniaId }); diff --git a/src/controllers/ProfileInsigniaEmployeeTempController.ts b/src/controllers/ProfileInsigniaEmployeeTempController.ts index 6f11cec3..56bf0c34 100644 --- a/src/controllers/ProfileInsigniaEmployeeTempController.ts +++ b/src/controllers/ProfileInsigniaEmployeeTempController.ts @@ -196,6 +196,45 @@ export class ProfileInsigniaEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลเครื่องราชอิสริยาภรณ์ + * @summary API ลบข้อมูลเครื่องราชอิสริยาภรณ์ + * @param insigniaId คีย์เครื่องราชอิสริยาภรณ์ + */ + @Patch("update-delete/{insigniaId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() insigniaId: string, + ) { + const record = await this.insigniaRepo.findOneBy({ id: insigniaId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileInsigniaHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileInsigniaId = insigniaId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.insigniaRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.insigniaHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{insigniaId}") public async deleteInsignia(@Path() insigniaId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileLeaveController.ts b/src/controllers/ProfileLeaveController.ts index a6624bbb..c61b3d95 100644 --- a/src/controllers/ProfileLeaveController.ts +++ b/src/controllers/ProfileLeaveController.ts @@ -265,6 +265,45 @@ export class ProfileLeaveController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการลา + * @summary API ลบข้อมูลการลา + * @param leaveId คีย์การลา + */ + @Patch("update-delete/{leaveId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() leaveId: string, + ) { + const record = await this.leaveRepo.findOneBy({ id: leaveId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileLeaveHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileLeaveId = leaveId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.leaveRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.leaveHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Patch("cancel/{leaveId}") public async updateCancel( @Request() req: RequestWithUser, diff --git a/src/controllers/ProfileLeaveEmployeeController.ts b/src/controllers/ProfileLeaveEmployeeController.ts index ade4a5c4..9f928776 100644 --- a/src/controllers/ProfileLeaveEmployeeController.ts +++ b/src/controllers/ProfileLeaveEmployeeController.ts @@ -192,6 +192,45 @@ export class ProfileLeaveEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการลา + * @summary API ลบข้อมูลการลา + * @param leaveId คีย์การลา + */ + @Patch("update-delete/{leaveId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() leaveId: string, + ) { + const record = await this.leaveRepo.findOneBy({ id: leaveId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileLeaveHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileLeaveId = leaveId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.leaveRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.leaveHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{leaveId}") public async deleteLeave(@Path() leaveId: string, @Request() req: RequestWithUser) { const _record = await this.leaveRepo.findOneBy({ id: leaveId }); diff --git a/src/controllers/ProfileLeaveEmployeeTempController.ts b/src/controllers/ProfileLeaveEmployeeTempController.ts index 85960fa5..a8aa979f 100644 --- a/src/controllers/ProfileLeaveEmployeeTempController.ts +++ b/src/controllers/ProfileLeaveEmployeeTempController.ts @@ -179,6 +179,45 @@ export class ProfileLeaveEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการลา + * @summary API ลบข้อมูลการลา + * @param leaveId คีย์การลา + */ + @Patch("update-delete/{leaveId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() leaveId: string, + ) { + const record = await this.leaveRepo.findOneBy({ id: leaveId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileLeaveHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileLeaveId = leaveId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.leaveRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.leaveHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{leaveId}") public async deleteLeave(@Path() leaveId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileNopaidController.ts b/src/controllers/ProfileNopaidController.ts index d0760d13..b9cecc1d 100644 --- a/src/controllers/ProfileNopaidController.ts +++ b/src/controllers/ProfileNopaidController.ts @@ -140,6 +140,45 @@ export class ProfileNopaidController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลบันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + * @summary API ลบข้อมูลบันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + * @param nopaidId คีย์บันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + */ + @Patch("update-delete/{nopaidId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() nopaidId: string, + ) { + const record = await this.nopaidRepository.findOneBy({ id: nopaidId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileNopaidHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileNopaidId = nopaidId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.nopaidRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.nopaidHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{nopaidId}") public async deleteNopaid(@Path() nopaidId: string, @Request() req: RequestWithUser) { const _record = await this.nopaidRepository.findOneBy({ id: nopaidId }); diff --git a/src/controllers/ProfileNopaidEmployeeController.ts b/src/controllers/ProfileNopaidEmployeeController.ts index e5849e8e..5a10ee75 100644 --- a/src/controllers/ProfileNopaidEmployeeController.ts +++ b/src/controllers/ProfileNopaidEmployeeController.ts @@ -147,6 +147,45 @@ export class ProfileNopaidEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลบันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + * @summary API ลบข้อมูลบันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + * @param nopaidId คีย์บันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + */ + @Patch("update-delete/{nopaidId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() nopaidId: string, + ) { + const record = await this.nopaidRepository.findOneBy({ id: nopaidId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileNopaidHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileNopaidId = nopaidId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.nopaidRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.nopaidHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{nopaidId}") public async deleteNopaid(@Path() nopaidId: string, @Request() req: RequestWithUser) { const _record = await this.nopaidRepository.findOneBy({ id: nopaidId }); diff --git a/src/controllers/ProfileNopaidEmployeeTempController.ts b/src/controllers/ProfileNopaidEmployeeTempController.ts index fe6edab3..ca9aa4a5 100644 --- a/src/controllers/ProfileNopaidEmployeeTempController.ts +++ b/src/controllers/ProfileNopaidEmployeeTempController.ts @@ -144,6 +144,45 @@ export class ProfileNopaidEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลบันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + * @summary API ลบข้อมูลบันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + * @param nopaidId คีย์บันทึกวันที่ไม่ได้รับเงินเงินเดือนฯ + */ + @Patch("update-delete/{nopaidId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() nopaidId: string, + ) { + const record = await this.nopaidRepository.findOneBy({ id: nopaidId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileNopaidHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileNopaidId = nopaidId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.nopaidRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.nopaidHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{nopaidId}") public async deleteNopaid(@Path() nopaidId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileOtherController.ts b/src/controllers/ProfileOtherController.ts index 430af83e..6f774c3b 100644 --- a/src/controllers/ProfileOtherController.ts +++ b/src/controllers/ProfileOtherController.ts @@ -149,6 +149,45 @@ export class ProfileOtherController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลข้อมูลอื่นๆ + * @summary API ลบข้อมูลข้อมูลอื่นๆ + * @param otherId คีย์ข้อมูลอื่นๆ + */ + @Patch("update-delete/{otherId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() otherId: string, + ) { + const record = await this.otherRepository.findOneBy({ id: otherId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + const before = structuredClone(record); + const history = new ProfileOtherHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileOtherId = otherId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.otherRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.otherHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{otherId}") public async deleteOther(@Path() otherId: string, @Request() req: RequestWithUser) { const _record = await this.otherRepository.findOneBy({ id: otherId }); diff --git a/src/controllers/ProfileOtherEmployeeController.ts b/src/controllers/ProfileOtherEmployeeController.ts index c8894afe..fe1adf94 100644 --- a/src/controllers/ProfileOtherEmployeeController.ts +++ b/src/controllers/ProfileOtherEmployeeController.ts @@ -160,6 +160,45 @@ export class ProfileOtherEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลข้อมูลอื่นๆ + * @summary API ลบข้อมูลข้อมูลอื่นๆ + * @param otherId คีย์ข้อมูลอื่นๆ + */ + @Patch("update-delete/{otherId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() otherId: string, + ) { + const record = await this.otherRepository.findOneBy({ id: otherId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileOtherHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileOtherId = otherId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.otherRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.otherHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{otherId}") public async deleteOther(@Path() otherId: string, @Request() req: RequestWithUser) { const _record = await this.otherRepository.findOneBy({ id: otherId }); diff --git a/src/controllers/ProfileOtherEmployeeTempController.ts b/src/controllers/ProfileOtherEmployeeTempController.ts index c20914fe..7f672ce1 100644 --- a/src/controllers/ProfileOtherEmployeeTempController.ts +++ b/src/controllers/ProfileOtherEmployeeTempController.ts @@ -148,6 +148,45 @@ export class ProfileOtherEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลข้อมูลอื่นๆ + * @summary API ลบข้อมูลข้อมูลอื่นๆ + * @param otherId คีย์ข้อมูลอื่นๆ + */ + @Patch("update-delete/{otherId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() otherId: string, + ) { + const record = await this.otherRepository.findOneBy({ id: otherId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileOtherHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileOtherId = otherId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.otherRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.otherHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{otherId}") public async deleteOther(@Path() otherId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); diff --git a/src/controllers/ProfileTrainingEmployeeController.ts b/src/controllers/ProfileTrainingEmployeeController.ts index d9f824a8..73bbc5cf 100644 --- a/src/controllers/ProfileTrainingEmployeeController.ts +++ b/src/controllers/ProfileTrainingEmployeeController.ts @@ -168,6 +168,45 @@ export class ProfileTrainingEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการฝึกอบรม/ดูงาน + * @summary API ลบข้อมูลการฝึกอบรม/ดูงาน + * @param trainingId คีย์การฝึกอบรม/ดูงาน + */ + @Patch("update-delete/{trainingId}") + public async updateIsDeletedTraining( + @Request() req: RequestWithUser, + @Path() trainingId: string, + ) { + const record = await this.trainingRepo.findOneBy({ id: trainingId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileTrainingHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileTrainingId = trainingId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.trainingRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.trainingHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{trainingId}") public async deleteTraining(@Path() trainingId: string, @Request() req: RequestWithUser) { const _record = await this.trainingRepo.findOneBy({ id: trainingId }); diff --git a/src/controllers/ProfileTrainingEmployeeTempController.ts b/src/controllers/ProfileTrainingEmployeeTempController.ts index 46e1686e..ace30f6d 100644 --- a/src/controllers/ProfileTrainingEmployeeTempController.ts +++ b/src/controllers/ProfileTrainingEmployeeTempController.ts @@ -156,6 +156,45 @@ export class ProfileTrainingEmployeeTempController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการฝึกอบรม/ดูงาน + * @summary API ลบข้อมูลการฝึกอบรม/ดูงาน + * @param trainingId คีย์การฝึกอบรม/ดูงาน + */ + @Patch("update-delete/{trainingId}") + public async updateIsDeletedTraining( + @Request() req: RequestWithUser, + @Path() trainingId: string, + ) { + const record = await this.trainingRepo.findOneBy({ id: trainingId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); + const before = structuredClone(record); + const history = new ProfileTrainingHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.profileTrainingId = trainingId; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.trainingRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.trainingHistoryRepo.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{trainingId}") public async deleteTraining(@Path() trainingId: string, @Request() req: RequestWithUser) { await new permission().PermissionDelete(req, "SYS_REGISTRY_TEMP"); From c5241b7a63618a7ecc949571ab53177d4404b7fc Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 18 Feb 2026 14:53:50 +0700 Subject: [PATCH 093/178] #1542 --- src/controllers/PosMasterActController.ts | 165 ++++++++++++++++------ 1 file changed, 120 insertions(+), 45 deletions(-) diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index b6e09e5b..dd4acd1b 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -109,68 +109,143 @@ export class PosMasterActController extends Controller { isAllRoot?: boolean; page?: number; pageSize?: number; + keyword?: string; }, ) { await new permission().PermissionGet(request, "SYS_ACTING"); - const { - page = 1, - pageSize = 100, - } = body + + const { page = 1, pageSize = 100, keyword } = body; + const posMasterMain = await this.posMasterRepository.findOne({ where: { id: body.posmasterId }, relations: ["posMasterActs"], }); - if (posMasterMain == null) { + + if (!posMasterMain) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); } - let posId: any = posMasterMain.posMasterActs.map((x) => x.posMasterChildId); + + let posId: any[] = posMasterMain.posMasterActs.map( + (x) => x.posMasterChildId + ); posId.push(body.posmasterId); - let typeCondition: any = {}; + + const query = await AppDataSource.getRepository(PosMaster) + .createQueryBuilder("posMaster") + .leftJoinAndSelect("posMaster.orgRoot", "orgRoot") + .leftJoinAndSelect("posMaster.orgChild1", "orgChild1") + .leftJoinAndSelect("posMaster.orgChild2", "orgChild2") + .leftJoinAndSelect("posMaster.orgChild3", "orgChild3") + .leftJoinAndSelect("posMaster.orgChild4", "orgChild4") + .leftJoinAndSelect("posMaster.current_holder", "current_holder") + .leftJoinAndSelect("current_holder.posLevel", "posLevel") + .leftJoinAndSelect("current_holder.posType", "posType") + .where("posMaster.current_holderId IS NOT NULL") + .andWhere("posMaster.id NOT IN (:...posId)", { posId }); + if (!body.isAllRoot) { - if (body.isAll == true) { - typeCondition = { - orgRootId: posMasterMain.orgRootId, - orgChild1Id: posMasterMain.orgChild1Id, - orgChild2Id: posMasterMain.orgChild2Id, - orgChild3Id: posMasterMain.orgChild3Id, - orgChild4Id: posMasterMain.orgChild4Id, - current_holderId: Not(IsNull()), - id: Not(In(posId)), - }; + if (body.isAll) { + if (posMasterMain.orgChild4Id) { + query.andWhere("posMaster.orgChild4Id = :id", { + id: posMasterMain.orgChild4Id, + }); + } else if (posMasterMain.orgChild3Id) { + query.andWhere("posMaster.orgChild3Id = :id", { + id: posMasterMain.orgChild3Id, + }); + } else if (posMasterMain.orgChild2Id) { + query.andWhere("posMaster.orgChild2Id = :id", { + id: posMasterMain.orgChild2Id, + }); + } else if (posMasterMain.orgChild1Id) { + query.andWhere("posMaster.orgChild1Id = :id", { + id: posMasterMain.orgChild1Id, + }); + } else { + query.andWhere("posMaster.orgRootId = :id", { + id: posMasterMain.orgRootId, + }); + } } else { - typeCondition = { - orgRootId: posMasterMain.orgRootId == null ? IsNull() : posMasterMain.orgRootId, - orgChild1Id: posMasterMain.orgChild1Id == null ? IsNull() : posMasterMain.orgChild1Id, - orgChild2Id: posMasterMain.orgChild2Id == null ? IsNull() : posMasterMain.orgChild2Id, - orgChild3Id: posMasterMain.orgChild3Id == null ? IsNull() : posMasterMain.orgChild3Id, - orgChild4Id: posMasterMain.orgChild4Id == null ? IsNull() : posMasterMain.orgChild4Id, - current_holderId: Not(IsNull()), - id: Not(In(posId)), - }; + query + .andWhere( + posMasterMain.orgRootId == null + ? "posMaster.orgRootId IS NULL" + : "posMaster.orgRootId = :orgRootId", + { orgRootId: posMasterMain.orgRootId } + ) + .andWhere( + posMasterMain.orgChild1Id == null + ? "posMaster.orgChild1Id IS NULL" + : "posMaster.orgChild1Id = :orgChild1Id", + { orgChild1Id: posMasterMain.orgChild1Id } + ) + .andWhere( + posMasterMain.orgChild2Id == null + ? "posMaster.orgChild2Id IS NULL" + : "posMaster.orgChild2Id = :orgChild2Id", + { orgChild2Id: posMasterMain.orgChild2Id } + ) + .andWhere( + posMasterMain.orgChild3Id == null + ? "posMaster.orgChild3Id IS NULL" + : "posMaster.orgChild3Id = :orgChild3Id", + { orgChild3Id: posMasterMain.orgChild3Id } + ) + .andWhere( + posMasterMain.orgChild4Id == null + ? "posMaster.orgChild4Id IS NULL" + : "posMaster.orgChild4Id = :orgChild4Id", + { orgChild4Id: posMasterMain.orgChild4Id } + ); } } else { - typeCondition = { + query.andWhere("posMaster.orgRootId = :orgRootId", { orgRootId: posMasterMain.orgRootId, - current_holderId: Not(IsNull()), - id: Not(In(posId)), - }; + }); } - const [posMaster, total] = await this.posMasterRepository.findAndCount({ - where: typeCondition, - relations: [ - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - "current_holder.posLevel", - "current_holder.posType", - ], - skip: (page - 1) * pageSize, - take: pageSize, - }); + if (keyword) { + query.andWhere( + new Brackets((qb) => { + qb.where( + `CONCAT(current_holder.prefix, current_holder.firstName, ' ', current_holder.lastName) LIKE :keyword`, + { keyword: `%${keyword}%` } + ) + .orWhere(`current_holder.citizenId LIKE :keyword`, { + keyword: `%${keyword}%`, + }) + .orWhere( + `CONCAT( + CASE + WHEN orgChild4.id IS NOT NULL THEN orgChild4.orgChild4ShortName + WHEN orgChild3.id IS NOT NULL THEN orgChild3.orgChild3ShortName + WHEN orgChild2.id IS NOT NULL THEN orgChild2.orgChild2ShortName + WHEN orgChild1.id IS NOT NULL THEN orgChild1.orgChild1ShortName + WHEN orgRoot.id IS NOT NULL THEN orgRoot.orgRootShortName + ELSE '' + END, + ' ', + posMaster.posMasterNo + ) LIKE :keyword`, + { keyword: `%${keyword}%` } + ) + .orWhere(`posLevel.posLevelName LIKE :keyword`, { + keyword: `%${keyword}%`, + }) + .orWhere(`posType.posTypeName LIKE :keyword`, { + keyword: `%${keyword}%`, + }) + .orWhere(`current_holder.position LIKE :keyword`, { + keyword: `%${keyword}%`, + }) + }) + ); + } + + query.skip((page - 1) * pageSize).take(pageSize); + + const [posMaster, total] = await query.getManyAndCount(); const data = await Promise.all( posMaster From c84b992c0c9c10f8dce5024a1e455fddc78dd0d7 Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 18 Feb 2026 15:50:55 +0700 Subject: [PATCH 094/178] #2315 --- src/controllers/CommandController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index b948289e..374dd56f 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -7386,7 +7386,7 @@ export class CommandController extends Controller { profile.employeeClass = "PERM"; const _null: any = null; profile.employeeWage = item.amount == null ? _null : item.amount.toString(); - profile.dateStart = new Date(); + profile.dateStart = _command ? _command.commandExcecuteDate : new Date(); profile.dateAppoint = new Date(); profile.amount = item.amount == null ? _null : item.amount; profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial; From df2f1c5b12570e52ae80c751933078a472a94708 Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 18 Feb 2026 15:57:15 +0700 Subject: [PATCH 095/178] fix --- src/controllers/CommandController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 374dd56f..34226f21 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -7387,7 +7387,7 @@ export class CommandController extends Controller { const _null: any = null; profile.employeeWage = item.amount == null ? _null : item.amount.toString(); profile.dateStart = _command ? _command.commandExcecuteDate : new Date(); - profile.dateAppoint = new Date(); + profile.dateAppoint = _command ? _command.commandExcecuteDate : new Date(); profile.amount = item.amount == null ? _null : item.amount; profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial; _reqBody.push({ From 71dcba33e9678f78c55985d33107fc9bc5c33609 Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 18 Feb 2026 18:09:20 +0700 Subject: [PATCH 096/178] migration and #2317 --- .../ProfileEmployeeTempController.ts | 864 +++++++++--------- src/entities/EmployeePosLevel.ts | 3 + src/entities/ProfileEmployee.ts | 6 +- ...1771409869898-add_relation_posLevelTemp.ts | 14 + 4 files changed, 452 insertions(+), 435 deletions(-) create mode 100644 src/migration/1771409869898-add_relation_posLevelTemp.ts diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index 096b3190..58536ccb 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -167,7 +167,7 @@ export class ProfileEmployeeTempController extends Controller { }, }); ImgUrl = response_.data.downloadUrl; - } catch {} + } catch { } } const province = await this.provinceRepository.findOneBy({ id: profile.registrationProvinceId, @@ -179,36 +179,36 @@ export class ProfileEmployeeTempController extends Controller { const root = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4; @@ -256,38 +256,38 @@ export class ProfileEmployeeTempController extends Controller { const salarys = salary_raw.length > 1 ? salary_raw.slice(1).map((item) => ({ - date: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiShortDate(item.commandDateAffect)) - : null, - position: Extension.ToThaiNumber( - Extension.ToThaiNumber( - `${item.positionName != null ? item.positionName : "-"} ${item.positionType == null ? item.positionCee ?? "" : (item.positionType == "อำนวยการ" || item.positionType == "บริหาร" ? item.positionType : "") + item.positionLevel}`, - ), + date: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiShortDate(item.commandDateAffect)) + : null, + position: Extension.ToThaiNumber( + Extension.ToThaiNumber( + `${item.positionName != null ? item.positionName : "-"} ${item.positionType == null ? item.positionCee ?? "" : (item.positionType == "อำนวยการ" || item.positionType == "บริหาร" ? item.positionType : "") + item.positionLevel}`, ), - posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : "", - orgRoot: item.orgRoot != null ? Extension.ToThaiNumber(item.orgRoot) : "", - orgChild1: item.orgChild1 != null ? Extension.ToThaiNumber(item.orgChild1) : "", - orgChild2: item.orgChild2 != null ? Extension.ToThaiNumber(item.orgChild2) : "", - orgChild3: item.orgChild3 != null ? Extension.ToThaiNumber(item.orgChild3) : "", - orgChild4: item.orgChild4 != null ? Extension.ToThaiNumber(item.orgChild4) : "", - positionCee: item.positionCee != null ? Extension.ToThaiNumber(item.positionCee) : "", - positionExecutive: - item.positionExecutive != null ? Extension.ToThaiNumber(item.positionExecutive) : "", - })) + ), + posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : "", + orgRoot: item.orgRoot != null ? Extension.ToThaiNumber(item.orgRoot) : "", + orgChild1: item.orgChild1 != null ? Extension.ToThaiNumber(item.orgChild1) : "", + orgChild2: item.orgChild2 != null ? Extension.ToThaiNumber(item.orgChild2) : "", + orgChild3: item.orgChild3 != null ? Extension.ToThaiNumber(item.orgChild3) : "", + orgChild4: item.orgChild4 != null ? Extension.ToThaiNumber(item.orgChild4) : "", + positionCee: item.positionCee != null ? Extension.ToThaiNumber(item.positionCee) : "", + positionExecutive: + item.positionExecutive != null ? Extension.ToThaiNumber(item.positionExecutive) : "", + })) : [ - { - date: "-", - position: "-", - posNo: "-", - orgRoot: null, - orgChild1: null, - orgChild2: null, - orgChild3: null, - orgChild4: null, - positionCee: null, - positionExecutive: null, - }, - ]; + { + date: "-", + position: "-", + posNo: "-", + orgRoot: null, + orgChild1: null, + orgChild2: null, + orgChild3: null, + orgChild4: null, + positionCee: null, + positionExecutive: null, + }, + ]; const educations = await this.profileEducationRepo.find({ select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], where: { profileEmployeeId: id, isDeleted: false }, @@ -296,20 +296,20 @@ export class ProfileEmployeeTempController extends Controller { const Education = educations && educations.length > 0 ? educations.map((item) => ({ - institute: item.institute ? item.institute : "-", - date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "-", - degree: item.degree && item.field ? `${item.degree} ${item.field}` : "-", - })) + institute: item.institute ? item.institute : "-", + date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "-", + degree: item.degree && item.field ? `${item.degree} ${item.field}` : "-", + })) : [ - { - institute: "-", - date: "-", - degree: "-", - }, - ]; + { + institute: "-", + date: "-", + degree: "-", + }, + ]; const mapData = { // Id: profile.id, @@ -347,10 +347,10 @@ export class ProfileEmployeeTempController extends Controller { position: salary_raw.length > 0 && salary_raw[0].positionName != null ? Extension.ToThaiNumber( - Extension.ToThaiNumber( - `${salary_raw[0].positionName != null ? salary_raw[0].positionName : "-"} ${salary_raw[0].positionType == null ? salary_raw[0].positionCee ?? "" : (salary_raw[0].positionType == "อำนวยการ" || salary_raw[0].positionType == "บริหาร" ? salary_raw[0].positionType : "") + salary_raw[0].positionLevel}`, - ), - ) + Extension.ToThaiNumber( + `${salary_raw[0].positionName != null ? salary_raw[0].positionName : "-"} ${salary_raw[0].positionType == null ? salary_raw[0].positionCee ?? "" : (salary_raw[0].positionType == "อำนวยการ" || salary_raw[0].positionType == "บริหาร" ? salary_raw[0].positionType : "") + salary_raw[0].positionLevel}`, + ), + ) : "", positionCee: salary_raw.length > 0 && salary_raw[0].positionCee != null @@ -360,27 +360,22 @@ export class ProfileEmployeeTempController extends Controller { salary_raw.length > 0 && salary_raw[0].positionExecutive != null ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].positionExecutive)) : "", - org: `${ - salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-" - ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild4)) + " " - : "" - }${ - salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-" + org: `${salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-" + ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild4)) + " " + : "" + }${salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild3)) + " " : "" - }${ - salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-" + }${salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild2)) + " " : "" - }${ - salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-" + }${salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild1)) + " " : "" - }${ - salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-" + }${salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgRoot)) : "" - }`, + }`, ocFullPath: (_child4 == null ? "" : _child4 + "\n") + (_child3 == null ? "" : _child3 + "\n") + @@ -453,7 +448,7 @@ export class ProfileEmployeeTempController extends Controller { }, }); _ImgUrl[i] = response_.data.downloadUrl; - } catch {} + } catch { } } }), ); @@ -467,7 +462,7 @@ export class ProfileEmployeeTempController extends Controller { }, }); ImgUrl = response_.data.downloadUrl; - } catch {} + } catch { } } const profileOc = await this.profileRepo.findOne({ relations: [ @@ -506,36 +501,36 @@ export class ProfileEmployeeTempController extends Controller { const root = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot; const child1 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1; const child2 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2; const child3 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3; const child4 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4; @@ -554,19 +549,19 @@ export class ProfileEmployeeTempController extends Controller { const certs = cert_raw.length > 0 ? cert_raw.slice(-2).map((item) => ({ - CertificateType: item.certificateType ?? null, - Issuer: item.issuer ?? null, - CertificateNo: Extension.ToThaiNumber(item.certificateNo) ?? null, - IssueDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) ?? null, - })) + CertificateType: item.certificateType ?? null, + Issuer: item.issuer ?? null, + CertificateNo: Extension.ToThaiNumber(item.certificateNo) ?? null, + IssueDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) ?? null, + })) : [ - { - CertificateType: "-", - Issuer: "-", - CertificateNo: "-", - IssueDate: "-", - }, - ]; + { + CertificateType: "-", + Issuer: "-", + CertificateNo: "-", + IssueDate: "-", + }, + ]; const training_raw = await this.trainingRepository.find({ select: ["startDate", "endDate", "place", "department"], where: { profileEmployeeId: id }, @@ -575,34 +570,34 @@ export class ProfileEmployeeTempController extends Controller { const trainings = training_raw.length > 0 ? training_raw.slice(-2).map((item) => ({ - Institute: item.department ?? "", - Start: - item.startDate == null - ? "" - : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)), - End: - item.endDate == null - ? "" - : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)), - Date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "", - Level: "", - Degree: item.name, - Field: "", - })) + Institute: item.department ?? "", + Start: + item.startDate == null + ? "" + : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)), + End: + item.endDate == null + ? "" + : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)), + Date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "", + Level: "", + Degree: item.name, + Field: "", + })) : [ - { - Institute: "-", - Start: "-", - End: "-", - Date: "-", - Level: "-", - Degree: "-", - Field: "-", - }, - ]; + { + Institute: "-", + Start: "-", + End: "-", + Date: "-", + Level: "-", + Degree: "-", + Field: "-", + }, + ]; const discipline_raw = await this.disciplineRepository.find({ select: ["refCommandDate", "refCommandNo", "detail"], @@ -612,19 +607,19 @@ export class ProfileEmployeeTempController extends Controller { const disciplines = discipline_raw.length > 0 ? discipline_raw.slice(-2).map((item) => ({ - DisciplineYear: - Extension.ToThaiNumber(new Date(item.refCommandDate).getFullYear().toString()) ?? - null, - DisciplineDetail: item.detail ?? null, - RefNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, - })) + DisciplineYear: + Extension.ToThaiNumber(new Date(item.refCommandDate).getFullYear().toString()) ?? + null, + DisciplineDetail: item.detail ?? null, + RefNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, + })) : [ - { - DisciplineYear: "-", - DisciplineDetail: "-", - RefNo: "-", - }, - ]; + { + DisciplineYear: "-", + DisciplineDetail: "-", + RefNo: "-", + }, + ]; const education_raw = await this.profileEducationRepo.find({ select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], @@ -635,34 +630,34 @@ export class ProfileEmployeeTempController extends Controller { const educations = education_raw.length > 0 ? education_raw.slice(-2).map((item) => ({ - Institute: item.institute, - Start: - item.startDate == null - ? "" - : Extension.ToThaiNumber(new Date(item.startDate).getFullYear().toString()), - End: - item.endDate == null - ? "" - : Extension.ToThaiNumber(new Date(item.endDate).getFullYear().toString()), - Date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "", - Level: item.educationLevel ?? "", - Degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", - Field: item.field ?? "-", - })) + Institute: item.institute, + Start: + item.startDate == null + ? "" + : Extension.ToThaiNumber(new Date(item.startDate).getFullYear().toString()), + End: + item.endDate == null + ? "" + : Extension.ToThaiNumber(new Date(item.endDate).getFullYear().toString()), + Date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "", + Level: item.educationLevel ?? "", + Degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", + Field: item.field ?? "-", + })) : [ - { - Institute: "-", - Start: "-", - End: "-", - Date: "-", - Level: "-", - Degree: "-", - Field: "-", - }, - ]; + { + Institute: "-", + Start: "-", + End: "-", + Date: "-", + Level: "-", + Degree: "-", + Field: "-", + }, + ]; const salary_raw = await this.salaryRepo.find({ select: [ "commandDateAffect", @@ -683,50 +678,50 @@ export class ProfileEmployeeTempController extends Controller { const salarys = salary_raw.length > 0 ? salary_raw.map((item) => ({ - SalaryDate: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + SalaryDate: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : null, + Position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, + PosNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : null, + Salary: + item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, + Special: + item.amountSpecial != null + ? Extension.ToThaiNumber(item.amountSpecial.toLocaleString()) : null, - Position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, - PosNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : null, - Salary: - item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, - Special: - item.amountSpecial != null - ? Extension.ToThaiNumber(item.amountSpecial.toLocaleString()) - : null, - Rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, - RefAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, - PositionLevel: - item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, - PositionType: item.positionType ?? null, - PositionAmount: - item.positionSalaryAmount == null - ? null - : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), - FullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, - OcFullPath: - (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root), - })) + Rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, + RefAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, + PositionLevel: + item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, + PositionType: item.positionType ?? null, + PositionAmount: + item.positionSalaryAmount == null + ? null + : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), + FullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, + OcFullPath: + (_child4 == null ? "" : _child4 + "\n") + + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root), + })) : [ - { - SalaryDate: "-", - Position: "-", - PosNo: "-", - Salary: "-", - Special: "-", - Rank: "-", - RefAll: "-", - PositionLevel: "-", - PositionType: "-", - PositionAmount: "-", - FullName: "-", - OcFullPath: "-", - }, - ]; + { + SalaryDate: "-", + Position: "-", + PosNo: "-", + Salary: "-", + Special: "-", + Rank: "-", + RefAll: "-", + PositionLevel: "-", + PositionType: "-", + PositionAmount: "-", + FullName: "-", + OcFullPath: "-", + }, + ]; const insignia_raw = await this.profileInsigniaRepo.find({ relations: { @@ -740,37 +735,37 @@ export class ProfileEmployeeTempController extends Controller { const insignias = insignia_raw.length > 0 ? insignia_raw.map((item) => ({ - ReceiveDate: item.receiveDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) - : "", - InsigniaName: item.insignia.name, - InsigniaShortName: item.insignia.shortName, - InsigniaTypeName: item.insignia.insigniaType.name, - No: item.no ? Extension.ToThaiNumber(item.no) : "", - Issue: item.issue ? item.issue : "", - VolumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", - Volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", - Section: item.section ? Extension.ToThaiNumber(item.section) : "", - Page: item.page ? Extension.ToThaiNumber(item.page) : "", - RefCommandDate: item.refCommandDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) - : "", - })) + ReceiveDate: item.receiveDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) + : "", + InsigniaName: item.insignia.name, + InsigniaShortName: item.insignia.shortName, + InsigniaTypeName: item.insignia.insigniaType.name, + No: item.no ? Extension.ToThaiNumber(item.no) : "", + Issue: item.issue ? item.issue : "", + VolumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", + Volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", + Section: item.section ? Extension.ToThaiNumber(item.section) : "", + Page: item.page ? Extension.ToThaiNumber(item.page) : "", + RefCommandDate: item.refCommandDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) + : "", + })) : [ - { - ReceiveDate: "-", - InsigniaName: "-", - InsigniaShortName: "-", - InsigniaTypeName: "-", - No: "-", - Issue: "-", - VolumeNo: "-", - Volume: "-", - Section: "-", - Page: "-", - RefCommandDate: "-", - }, - ]; + { + ReceiveDate: "-", + InsigniaName: "-", + InsigniaShortName: "-", + InsigniaTypeName: "-", + No: "-", + Issue: "-", + VolumeNo: "-", + Volume: "-", + Section: "-", + Page: "-", + RefCommandDate: "-", + }, + ]; const leave_raw = await this.profileLeaveRepository.find({ relations: { leaveType: true }, @@ -780,19 +775,19 @@ export class ProfileEmployeeTempController extends Controller { const leaves = leave_raw.length > 0 ? leave_raw.map((item) => ({ - LeaveTypeName: item.leaveType.name, - DateLeaveStart: item.dateLeaveStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) - : "", - LeaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "", - })) + LeaveTypeName: item.leaveType.name, + DateLeaveStart: item.dateLeaveStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + : "", + LeaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "", + })) : [ - { - LeaveTypeName: "-", - DateLeaveStart: "-", - LeaveDays: "-", - }, - ]; + { + LeaveTypeName: "-", + DateLeaveStart: "-", + LeaveDays: "-", + }, + ]; const data = { fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, @@ -819,20 +814,20 @@ export class ProfileEmployeeTempController extends Controller { profiles.citizenId != null ? Extension.ToThaiNumber(profiles.citizenId.toString()) : "", fatherFullName: profileFamilyFather?.fatherPrefix || - profileFamilyFather?.fatherFirstName || - profileFamilyFather?.fatherLastName + profileFamilyFather?.fatherFirstName || + profileFamilyFather?.fatherLastName ? `${profileFamilyFather?.fatherPrefix ?? ""}${profileFamilyFather?.fatherFirstName ?? ""} ${profileFamilyFather?.fatherLastName ?? ""}`.trim() : null, motherFullName: profileFamilyMother?.motherPrefix || - profileFamilyMother?.motherFirstName || - profileFamilyMother?.motherLastName + profileFamilyMother?.motherFirstName || + profileFamilyMother?.motherLastName ? `${profileFamilyMother?.motherPrefix ?? ""}${profileFamilyMother?.motherFirstName ?? ""} ${profileFamilyMother?.motherLastName ?? ""}`.trim() : null, coupleFullName: profileFamilyCouple?.couplePrefix || - profileFamilyCouple?.coupleFirstName || - profileFamilyCouple?.coupleLastNameOld + profileFamilyCouple?.coupleFirstName || + profileFamilyCouple?.coupleLastNameOld ? `${profileFamilyCouple?.couplePrefix ?? ""}${profileFamilyCouple?.coupleFirstName ?? ""} ${profileFamilyCouple?.coupleLastName ?? ""}`.trim() : null, coupleLastNameOld: profileFamilyCouple?.coupleLastNameOld ?? null, @@ -1180,8 +1175,8 @@ export class ProfileEmployeeTempController extends Controller { _data.profileEmployeeEmployment.length == 0 ? null : _data.profileEmployeeEmployment.reduce((latest, current) => { - return latest.date > current.date ? latest : current; - }).date; + return latest.date > current.date ? latest : current; + }).date; return { id: _data.id, prefix: _data.prefix, @@ -1331,32 +1326,32 @@ export class ProfileEmployeeTempController extends Controller { profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = profile.current_holders.length == 0 || - (profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) + (profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; @@ -1541,7 +1536,7 @@ export class ProfileEmployeeTempController extends Controller { let query = await this.profileRepo .createQueryBuilder("profileEmployee") - .leftJoinAndSelect("profileEmployee.posLevel", "posLevel") + .leftJoinAndSelect("profileEmployee.posLevelTemp", "posLevelTemp") .leftJoinAndSelect("profileEmployee.posType", "posType") .leftJoinAndSelect("profileEmployee.current_holderTemps", "current_holderTemps") .leftJoinAndSelect("profileEmployee.profileEmployeeEmployment", "profileEmployeeEmployment") @@ -1619,7 +1614,7 @@ export class ProfileEmployeeTempController extends Controller { ) .andWhere( posLevel != undefined && posLevel != null && posLevel != "" - ? "posLevel.posLevelName LIKE :keyword2" + ? "posLevelTemp.posLevelName LIKE :keyword2" : "1=1", { keyword2: `${posLevel}`, @@ -1654,8 +1649,8 @@ export class ProfileEmployeeTempController extends Controller { ); if (sortBy) { - if (sortBy == "posLevel") { - query = query.orderBy(`posLevel.posLevelName`, descending ? "DESC" : "ASC"); + if (sortBy == "posLevelTemp") { + query = query.orderBy(`posLevelTemp.posLevelName`, descending ? "DESC" : "ASC"); } else if (sortBy == "posType") { query = query.orderBy(`posType.posTypeName`, descending ? "DESC" : "ASC"); } else if (sortBy == "govAge") { @@ -1676,35 +1671,35 @@ export class ProfileEmployeeTempController extends Controller { _data.current_holderTemps.length == 0 ? null : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild4 != null + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild4 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild3 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + null && + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + null && + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const dateEmployment = _data.profileEmployeeEmployment.length == 0 ? null : _data.profileEmployeeEmployment.reduce((latest, current) => { - return latest.date > current.date ? latest : current; - }).date; + return latest.date > current.date ? latest : current; + }).date; return { id: _data.id, prefix: _data.prefix, @@ -1712,7 +1707,8 @@ export class ProfileEmployeeTempController extends Controller { firstName: _data.firstName, lastName: _data.lastName, citizenId: _data.citizenId, - posLevel: _data.posLevel == null ? null : _data.posLevel.posLevelName, + // posLevel: _data.posLevel == null ? null : _data.posLevel.posLevelName, + posLevel: _data.posLevelTemp == null ? null : _data.posLevelTemp.posLevelName, posType: _data.posType == null ? null : _data.posType.posTypeName, posTypeShortName: _data.posType == null ? null : _data.posType.posTypeShortName, posLevelId: _data.posLevel == null ? null : _data.posLevel.id, @@ -1953,8 +1949,8 @@ export class ProfileEmployeeTempController extends Controller { .map((x) => x.current_holderId).length == 0 ? ["zxc"] : orgRevision.employeeTempPosMasters - .filter((x) => x.current_holderId != null) - .map((x) => x.current_holderId), + .filter((x) => x.current_holderId != null) + .map((x) => x.current_holderId), }); }), ) @@ -2123,74 +2119,74 @@ export class ProfileEmployeeTempController extends Controller { posTypeId: profile.posType == null ? null : profile.posType.id, rootId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRootId, + ?.orgRootId, root: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot - .orgRootName, + .orgRootName, child1Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild1Id, + ?.orgChild1Id, child1: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 - .orgChild1Name, + .orgChild1Name, child2Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild2Id, + ?.orgChild2Id, child2: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 - .orgChild2Name, + .orgChild2Name, child3Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild3Id, + ?.orgChild3Id, child3: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 - .orgChild3Name, + .orgChild3Name, child4Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild4Id, + ?.orgChild4Id, child4: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 - .orgChild4Name, + .orgChild4Name, salary: profile ? profile.amount : null, amountSpecial: profile ? profile.amountSpecial : null, }; @@ -2318,34 +2314,34 @@ export class ProfileEmployeeTempController extends Controller { item.current_holderTemps.length == 0 ? null : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild4 != null + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild4 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild3 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = item.current_holderTemps.length == 0 || - (item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == + (item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; @@ -2831,54 +2827,54 @@ export class ProfileEmployeeTempController extends Controller { isProbation: item.isProbation, orgRootName: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot + ?.orgRootName == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName, + ?.orgRootName, orgChild1Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 - ?.orgChild1Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 + ?.orgChild1Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild1?.orgChild1Name, + ?.orgChild1?.orgChild1Name, orgChild2Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 - ?.orgChild2Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 + ?.orgChild2Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2?.orgChild2Name, + ?.orgChild2?.orgChild2Name, orgChild3Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 - ?.orgChild3Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 + ?.orgChild3Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3?.orgChild3Name, + ?.orgChild3?.orgChild3Name, orgChild4Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 - ?.orgChild4Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 + ?.orgChild4Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4?.orgChild4Name, + ?.orgChild4?.orgChild4Name, }; }), ); @@ -3097,7 +3093,7 @@ export class ProfileEmployeeTempController extends Controller { isLeave: false, isRetired: item.current_holder.birthDate == null || - calculateRetireDate(item.current_holder.birthDate).getFullYear() != body.year + calculateRetireDate(item.current_holder.birthDate).getFullYear() != body.year ? false : true, isSpecial: false, @@ -3153,68 +3149,68 @@ export class ProfileEmployeeTempController extends Controller { posTypeId: profile.posType == null ? null : profile.posType.id, rootId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRootId, root: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot.orgRootName, child1Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1Id, child1: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 - .orgChild1Name, + .orgChild1Name, child2Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2Id, child2: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 - .orgChild2Name, + .orgChild2Name, child3Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3Id, child3: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 - .orgChild3Name, + .orgChild3Name, child4Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4Id, child4: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 - .orgChild4Name, + .orgChild4Name, }; return new HttpSuccess(_profile); } @@ -3267,8 +3263,8 @@ export class ProfileEmployeeTempController extends Controller { const formattedData = profiles.map((item) => { const posMaster = item.current_holders == null || - item.current_holders.length == 0 || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null + item.current_holders.length == 0 || + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id); // const posExecutive = @@ -3293,49 +3289,49 @@ export class ProfileEmployeeTempController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; const child1 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1; const child2 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2; const child3 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3; const child4 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4; @@ -3459,24 +3455,24 @@ export class ProfileEmployeeTempController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; const dest_item = await this.salaryRepo.findOne({ @@ -3918,7 +3914,7 @@ export class ProfileEmployeeTempController extends Controller { positionId: profile.positionIdTemp, profileId: profile.id, }) - .then(async () => {}); + .then(async () => { }); } }), ); @@ -4009,32 +4005,32 @@ export class ProfileEmployeeTempController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = item.current_holders.length == 0 || - (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) + (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; @@ -4127,36 +4123,36 @@ export class ProfileEmployeeTempController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -4170,27 +4166,27 @@ export class ProfileEmployeeTempController extends Controller { profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild4 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild4 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild2 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : null; const _profile: any = { diff --git a/src/entities/EmployeePosLevel.ts b/src/entities/EmployeePosLevel.ts index 39ba8a99..02759073 100644 --- a/src/entities/EmployeePosLevel.ts +++ b/src/entities/EmployeePosLevel.ts @@ -52,6 +52,9 @@ export class EmployeePosLevel extends EntityBase { @OneToMany(() => ProfileEmployee, (profile) => profile.posLevel) profiles: ProfileEmployee[]; + + @OneToMany(() => ProfileEmployee, (profile) => profile.posLevelTemp) + profilesTemp: ProfileEmployee[]; } export class CreateEmployeePosLevel { diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index e73ca914..3396d18c 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -1,4 +1,4 @@ -import { Entity, Column, OneToMany, ManyToOne, Double, ManyToMany, JoinTable } from "typeorm"; +import { Entity, Column, OneToMany, ManyToOne, Double, ManyToMany, JoinTable, JoinColumn } from "typeorm"; import { EntityBase } from "./base/Base"; import { EmployeePosLevel } from "./EmployeePosLevel"; import { EmployeePosType } from "./EmployeePosType"; @@ -792,6 +792,10 @@ export class ProfileEmployee extends EntityBase { @ManyToOne(() => EmployeePosLevel, (v) => v.profiles) posLevel: EmployeePosLevel; + @ManyToOne(() => EmployeePosLevel, (v) => v.profilesTemp) + @JoinColumn({ name: "posLevelIdTemp" }) + posLevelTemp: EmployeePosLevel; + @ManyToOne(() => EmployeePosType, (v) => v.profiles) posType: EmployeePosType; diff --git a/src/migration/1771409869898-add_relation_posLevelTemp.ts b/src/migration/1771409869898-add_relation_posLevelTemp.ts new file mode 100644 index 00000000..3d7dfce3 --- /dev/null +++ b/src/migration/1771409869898-add_relation_posLevelTemp.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddRelationPosLevelTemp1771409869898 implements MigrationInterface { + name = 'AddRelationPosLevelTemp1771409869898' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileEmployee\` ADD CONSTRAINT \`FK_49694ac4248a7bab7d12d4be280\` FOREIGN KEY (\`posLevelIdTemp\`) REFERENCES \`employeePosLevel\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileEmployee\` DROP FOREIGN KEY \`FK_49694ac4248a7bab7d12d4be280\``); + } + +} From c8ed816a1f3dcf588b912a843a3a33dc381779ec Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 19 Feb 2026 09:32:17 +0700 Subject: [PATCH 097/178] =?UTF-8?q?filter=20case=20call=20leave=20server?= =?UTF-8?q?=20error=20#2304=20&=20=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=E0=B8=95=E0=B8=B1=E0=B8=A7=E0=B9=80=E0=B8=A5=E0=B8=B7?= =?UTF-8?q?=E0=B8=AD=E0=B8=81=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=84=E0=B8=A3?= =?UTF-8?q?=E0=B8=AD=E0=B8=87-=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=87=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB?= =?UTF-8?q?=E0=B8=99=E0=B9=88=E0=B8=87=20#2311?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 80 ++++++---------------------- src/controllers/ReportController.ts | 77 ++++++-------------------- 2 files changed, 32 insertions(+), 125 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index b948289e..9a2592fa 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -5821,7 +5821,6 @@ export class CommandController extends Controller { body.data.map(async (item) => { const profile = await this.profileRepository.findOne({ relations: [ - // "profileSalary", "posType", "posLevel", "current_holders", @@ -5832,11 +5831,6 @@ export class CommandController extends Controller { "current_holders.orgChild4", ], where: { id: item.profileId }, - // order: { - // profileSalary: { - // order: "DESC", - // }, - // }, }); if (!profile) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); @@ -5848,46 +5842,19 @@ export class CommandController extends Controller { }); const nextOrder = lastSalary ? lastSalary.order + 1 : 1; const orgRevision = await this.orgRevisionRepo.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); const orgRevisionRef = profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - const orgRootRef = orgRevisionRef?.orgRoot ?? null; - const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; const shortName = - !profile.current_holders || profile.current_holders.length == 0 - ? null - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` - : null; - const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; + orgRevisionRef?.orgChild4?.orgChild4ShortName ?? + orgRevisionRef?.orgChild3?.orgChild3ShortName ?? + orgRevisionRef?.orgChild2?.orgChild2ShortName ?? + orgRevisionRef?.orgChild1?.orgChild1ShortName ?? + orgRevisionRef?.orgRoot?.orgRootShortName ?? + null; + const posNo = orgRevisionRef?.posMasterNo?.toString() ?? null; let position = profile.current_holders .filter((x) => x.orgRevisionId == orgRevision?.id)[0] @@ -5907,18 +5874,12 @@ export class CommandController extends Controller { amountSpecial: item.amountSpecial ? item.amountSpecial : null, positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - // order: - // profile.profileSalary.length >= 0 - // ? profile.profileSalary.length > 0 - // ? profile.profileSalary[0].order + 1 - // : 1 - // : null, order: nextOrder, - orgRoot: orgRootRef?.orgRootName ?? null, - orgChild1: orgChild1Ref?.orgChild1Name ?? null, - orgChild2: orgChild2Ref?.orgChild2Name ?? null, - orgChild3: orgChild3Ref?.orgChild3Name ?? null, - orgChild4: orgChild4Ref?.orgChild4Name ?? null, + orgRoot: orgRevisionRef?.orgRoot?.orgRootName ?? null, + orgChild1: orgRevisionRef?.orgChild1?.orgChild1Name ?? null, + orgChild2: orgRevisionRef?.orgChild2?.orgChild2Name ?? null, + orgChild3: orgRevisionRef?.orgChild3?.orgChild3Name ?? null, + orgChild4: orgRevisionRef?.orgChild4?.orgChild4Name ?? null, createdUserId: req.user.sub, createdFullName: req.user.name, lastUpdateUserId: req.user.sub, @@ -5944,19 +5905,8 @@ export class CommandController extends Controller { await this.salaryHistoryRepo.save(history); }), ); - // const checkCommandType = await this.commandRepository.findOne({ - // where: { id: body.data.length > 0 ? body.data[0].commandId?.toString() : "" }, - // relations: ["commandType"], - // }); + if (commandType && String(commandType.code) == "C-PM-11") { - // const profile = await this.profileRepository.find({ - // where: { id: In(body.data.map((x) => x.profileId)) }, - // }); - // const data = profile.map((x) => ({ - // ...x, - // isProbation: false, - // })); - // await this.profileRepository.save(data); const profileIds = body.data.map((x) => x.profileId); await this.profileRepository.update( { id: In(profileIds) }, @@ -5976,6 +5926,8 @@ export class CommandController extends Controller { beginningLeaveDays: 0, beginningLeaveCount: 0, }) + .then(() => {}) + .catch(() => {}) ) ); } diff --git a/src/controllers/ReportController.ts b/src/controllers/ReportController.ts index 3966fb21..da96d091 100644 --- a/src/controllers/ReportController.ts +++ b/src/controllers/ReportController.ts @@ -87,6 +87,7 @@ export class ReportController extends Controller { @Query() isProbation?: boolean, @Query() isRetire?: boolean, @Query() isRetireLaw?: boolean, + @Query() isCurrent?: boolean, @Query() retireType?: string, @Query() tenureType?: string, @Query() tenureMin?: number, @@ -253,62 +254,16 @@ export class ReportController extends Controller { .andWhere(field != null && field != "" ? "registryOfficer.fields LIKE :fields" : "1=1", { fields: `%${field}%`, }) + .andWhere( + isCurrent === undefined || isCurrent === null + ? "1=1" + : isCurrent === true + ? "registryOfficer.posMasterNo IS NOT NULL" + : "registryOfficer.posMasterNo IS NULL", + ) .orderBy(`registryOfficer.${sortBy}`, sort) .getManyAndCount(); - // const mapData1 = await Promise.all( - // lists.map(async (x) => { - // return { - // profileId: x.profileId, - // citizenId: x.citizenId, - // prefix: x.prefix, - // firstName: x.firstName, - // lastName: x.lastName, - // isProbation: x.isProbation, - // isLeave: x.isLeave, - // isRetirement: x.isRetirement, - // leaveType: x.leaveType, - // posMasterNo: x.posMasterNo, - // orgRootId: x.orgRootId, - // orgChild1Id: x.orgChild1Id, - // orgChild2Id: x.orgChild2Id, - // orgChild3Id: x.orgChild3Id, - // orgChild4Id: x.orgChild4Id, - // orgRootName: x.orgRootName, - // orgChild1Name: x.orgChild1Name, - // orgChild2Name: x.orgChild2Name, - // orgChild3Name: x.orgChild3Name, - // orgChild4Name: x.orgChild4Name, - // org: x.org, - // searchShortName: x.searchShortName, - // posExecutiveName: x.posExecutiveName, - // position: x.position, - // posTypeName: x.posTypeName, - // posLevelName: x.posLevelName, - // gender: x.gender, - // relationship: x.relationship, - // dateAppoint: x.dateAppoint, - // dateRetire: x.dateRetire, - // dateRetireLaw: x.dateRetireLaw, - // birthdate: x.birthdate, - // degree: x.degrees, - // age: x.age, - // currentPosition: null, - // lengthPosition: null, - // positionDate: { - // Years: x.Years ? x.Years : 0, - // Months: x.Months ? x.Months : 0, - // Days: x.Days ? x.Days : 0, - // }, - // levelDate: { - // posExecutiveYears: x.levelYears, - // posExecutiveMonths: x.levelMonths, - // posExecutiveDays: x.levelDays, - // }, - // }; - // }), - // ); - const mapData = []; for await (const x of lists) { let _educations: any = []; @@ -322,11 +277,6 @@ export class ReportController extends Controller { Array.isArray(x.Educations) && x.Educations != null ? (x.Educations as any[]).filter((i: any) => i.isHigh == true) : []; - // if(_educations.length == 0) { - // _educations = Array.isArray(x.Educations) && x.Educations != null - // ? (x.Educations as any[])[0] - // : [] - // } } } else { _educations = @@ -379,9 +329,6 @@ export class ReportController extends Controller { dateRetireLaw: x.dateRetireLaw, birthdate: x.birthdate, Educations: _educations, - // degree: x.degrees, - // educationLevel: x.educationLevels, - // field: x.fields, age: x.age, currentPosition: null, lengthPosition: null, @@ -429,6 +376,7 @@ export class ReportController extends Controller { @Query() isProbation?: boolean, @Query() isRetire?: boolean, @Query() isRetireLaw?: boolean, + @Query() isCurrent?: boolean, @Query() retireType?: string, @Query() ageMin?: number, @Query() ageMax?: number, @@ -581,6 +529,13 @@ export class ReportController extends Controller { .andWhere(field != null && field != "" ? "registryEmployee.fields LIKE :fields" : "1=1", { fields: `%${field}%`, }) + .andWhere( + isCurrent === undefined || isCurrent === null + ? "1=1" + : isCurrent === true + ? "registryEmployee.posMasterNo IS NOT NULL" + : "registryEmployee.posMasterNo IS NULL", + ) .orderBy(`registryEmployee.${sortBy}`, sort) .getManyAndCount(); From 525a885e133b48e2171477b131d3be03b745e5ab Mon Sep 17 00:00:00 2001 From: Adisak Date: Thu, 19 Feb 2026 10:36:32 +0700 Subject: [PATCH 098/178] migration and #2317(2) --- src/controllers/ProfileEmployeeTempController.ts | 13 +++++++------ src/entities/EmployeePosType.ts | 3 +++ src/entities/ProfileEmployee.ts | 4 ++++ .../1771470195684-add_relation_posTypeTemp.ts | 15 +++++++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/migration/1771470195684-add_relation_posTypeTemp.ts diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index 58536ccb..6bde1171 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -1537,7 +1537,7 @@ export class ProfileEmployeeTempController extends Controller { let query = await this.profileRepo .createQueryBuilder("profileEmployee") .leftJoinAndSelect("profileEmployee.posLevelTemp", "posLevelTemp") - .leftJoinAndSelect("profileEmployee.posType", "posType") + .leftJoinAndSelect("profileEmployee.posTypeTemp", "posTypeTemp") .leftJoinAndSelect("profileEmployee.current_holderTemps", "current_holderTemps") .leftJoinAndSelect("profileEmployee.profileEmployeeEmployment", "profileEmployeeEmployment") .leftJoinAndSelect("current_holderTemps.positions", "positions") @@ -1606,7 +1606,7 @@ export class ProfileEmployeeTempController extends Controller { ) .andWhere( posType != undefined && posType != null && posType != "" - ? "posType.posTypeName LIKE :keyword1" + ? "posTypeTemp.posTypeName LIKE :keyword1" : "1=1", { keyword1: `${posType}`, @@ -1709,10 +1709,11 @@ export class ProfileEmployeeTempController extends Controller { citizenId: _data.citizenId, // posLevel: _data.posLevel == null ? null : _data.posLevel.posLevelName, posLevel: _data.posLevelTemp == null ? null : _data.posLevelTemp.posLevelName, - posType: _data.posType == null ? null : _data.posType.posTypeName, - posTypeShortName: _data.posType == null ? null : _data.posType.posTypeShortName, - posLevelId: _data.posLevel == null ? null : _data.posLevel.id, - posTypeId: _data.posType == null ? null : _data.posType.id, + // posType: _data.posType == null ? null : _data.posType.posTypeName, + posType: _data.posTypeTemp == null ? null : _data.posTypeTemp.posTypeName, + posTypeShortName: _data.posTypeTemp == null ? null : _data.posTypeTemp.posTypeShortName, + posLevelId: _data.posLevelTemp == null ? null : _data.posLevelTemp.id, + posTypeId: _data.posTypeTemp == null ? null : _data.posTypeTemp.id, positionId: _data.positionIdTemp, posmasterId: _data.posmasterIdTemp, position: _data.position, diff --git a/src/entities/EmployeePosType.ts b/src/entities/EmployeePosType.ts index 2fd02b2e..3e827fe6 100644 --- a/src/entities/EmployeePosType.ts +++ b/src/entities/EmployeePosType.ts @@ -39,6 +39,9 @@ export class EmployeePosType extends EntityBase { @OneToMany(() => ProfileEmployee, (profile) => profile.posType) profiles: ProfileEmployee[]; + + @OneToMany(() => ProfileEmployee, (profile) => profile.posTypeTemp) + profilesTemp: ProfileEmployee[]; } export class CreateEmployeePosType { diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index 3396d18c..50b6d78f 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -796,6 +796,10 @@ export class ProfileEmployee extends EntityBase { @JoinColumn({ name: "posLevelIdTemp" }) posLevelTemp: EmployeePosLevel; + @ManyToOne(() => EmployeePosType, (v) => v.profilesTemp) + @JoinColumn({ name: "posTypeIdTemp" }) + posTypeTemp: EmployeePosType; + @ManyToOne(() => EmployeePosType, (v) => v.profiles) posType: EmployeePosType; diff --git a/src/migration/1771470195684-add_relation_posTypeTemp.ts b/src/migration/1771470195684-add_relation_posTypeTemp.ts new file mode 100644 index 00000000..4772ded9 --- /dev/null +++ b/src/migration/1771470195684-add_relation_posTypeTemp.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddRelationPosTypeTemp1771470195684 implements MigrationInterface { + name = 'AddRelationPosTypeTemp1771470195684' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileEmployee\` ADD CONSTRAINT \`FK_bc2f7791abcc1e55a73b99216ca\` FOREIGN KEY (\`posTypeIdTemp\`) REFERENCES \`employeePosType\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileEmployee\` DROP FOREIGN KEY \`FK_bc2f7791abcc1e55a73b99216ca\``); + } + +} From f1d9831055246db26aaa799bc28f7cdb10194174 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 19 Feb 2026 13:35:12 +0700 Subject: [PATCH 099/178] comment call leave service --- src/controllers/CommandController.ts | 46 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 25ef41bb..dd369b64 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -5813,10 +5813,10 @@ export class CommandController extends Controller { .orgRootShortName ?? ""; } } - const leaveType = await this.leaveType.findOne({ - select: { id: true, limit: true, code: true }, - where: { code: "LV-005" } - }); + // const leaveType = await this.leaveType.findOne({ + // select: { id: true, limit: true, code: true }, + // where: { code: "LV-005" } + // }); await Promise.all( body.data.map(async (item) => { const profile = await this.profileRepository.findOne({ @@ -5912,25 +5912,25 @@ export class CommandController extends Controller { { id: In(profileIds) }, { isProbation: false } ); - // Task #2304 อัปเดตจำนวนสิทธิ์การลา เมื่อผ่านทดลองงานฯ - if (leaveType != null) { - await Promise.all( - body.data.map((item) => - new CallAPI().PutData(req, `/leave-beginning/schedule`, { - profileId: item.profileId, - leaveTypeId: leaveType.id, - leaveYear: item.commandYear, - leaveDays: leaveType.limit, - leaveDaysUsed: 0, - leaveCount: 0, - beginningLeaveDays: 0, - beginningLeaveCount: 0, - }) - .then(() => {}) - .catch(() => {}) - ) - ); - } + // // Task #2304 อัปเดตจำนวนสิทธิ์การลา เมื่อผ่านทดลองงานฯ + // if (leaveType != null) { + // await Promise.all( + // body.data.map((item) => + // new CallAPI().PutData(req, `/leave-beginning/schedule`, { + // profileId: item.profileId, + // leaveTypeId: leaveType.id, + // leaveYear: item.commandYear, + // leaveDays: leaveType.limit, + // leaveDaysUsed: 0, + // leaveCount: 0, + // beginningLeaveDays: 0, + // beginningLeaveCount: 0, + // }) + // .then(() => {}) + // .catch(() => {}) + // ) + // ); + // } } return new HttpSuccess(); } From da75287882b4b3667451a38b7ddb3668c1a5f39f Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 19 Feb 2026 13:36:07 +0700 Subject: [PATCH 100/178] no message --- src/controllers/PositionController.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index c2facbd4..3a42acce 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -2003,7 +2003,7 @@ export class PositionController extends Controller { posMaster.orgChild3Id == null ) { body.type = 0; - shortName = posMaster.orgRoot.orgRootShortName; + shortName = posMaster.orgRoot?.orgRootShortName; } else if ( posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && @@ -2011,7 +2011,7 @@ export class PositionController extends Controller { posMaster.orgChild3Id == null ) { body.type = 1; - shortName = posMaster.orgChild1.orgChild1ShortName; + shortName = posMaster.orgChild1?.orgChild1ShortName; } else if ( posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && @@ -2019,7 +2019,7 @@ export class PositionController extends Controller { posMaster.orgChild3Id == null ) { body.type = 2; - shortName = posMaster.orgChild2.orgChild2ShortName; + shortName = posMaster.orgChild2?.orgChild2ShortName; } else if ( posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && @@ -2027,7 +2027,7 @@ export class PositionController extends Controller { posMaster.orgChild3Id !== null ) { body.type = 3; - shortName = posMaster.orgChild3.orgChild3ShortName; + shortName = posMaster.orgChild3?.orgChild3ShortName; } else if ( posMaster.orgRootId !== null && posMaster.orgChild1Id !== null && @@ -2035,7 +2035,7 @@ export class PositionController extends Controller { posMaster.orgChild3Id !== null ) { body.type = 4; - shortName = posMaster.orgChild4.orgChild4ShortName; + shortName = posMaster.orgChild4?.orgChild4ShortName; } return { From caacf07c76c84acfa372082b82dd317b263d1706 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 19 Feb 2026 14:27:02 +0700 Subject: [PATCH 101/178] =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B9=84?= =?UTF-8?q?=E0=B8=82=E0=B8=AA=E0=B8=B4=E0=B8=97=E0=B8=98=E0=B8=B4=E0=B9=8C?= =?UTF-8?q?=20PARENT=20=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B9=80=E0=B8=AB?= =?UTF-8?q?=E0=B9=87=E0=B8=99=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=B9?= =?UTF-8?q?=E0=B8=A5=E0=B8=97=E0=B8=B1=E0=B9=89=E0=B8=87=E0=B8=AB=E0=B8=A1?= =?UTF-8?q?=E0=B8=94=E0=B8=97=E0=B8=B8=E0=B8=81=E0=B8=AB=E0=B8=99=E0=B9=88?= =?UTF-8?q?=E0=B8=A7=E0=B8=A2=E0=B8=87=E0=B8=B2=E0=B8=99=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 3 +- .../DevelopmentRequestController.ts | 3 +- src/controllers/EmployeePositionController.ts | 3 +- .../EmployeeTempPositionController.ts | 3 +- .../OrganizationDotnetController.ts | 120 ++++++++++-------- src/controllers/PermissionController.ts | 6 +- src/controllers/PositionController.ts | 3 +- src/controllers/ProfileController.ts | 15 ++- src/controllers/ProfileEditController.ts | 3 +- .../ProfileEditEmployeeController.ts | 3 +- src/controllers/ProfileEmployeeController.ts | 9 +- .../ProfileEmployeeTempController.ts | 9 +- .../ProfileSalaryTempController.ts | 6 +- src/interfaces/permission.ts | 6 +- 14 files changed, 113 insertions(+), 79 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index dd369b64..5911940e 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -219,7 +219,8 @@ export class CommandController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, diff --git a/src/controllers/DevelopmentRequestController.ts b/src/controllers/DevelopmentRequestController.ts index f02b3049..14c485c4 100644 --- a/src/controllers/DevelopmentRequestController.ts +++ b/src/controllers/DevelopmentRequestController.ts @@ -165,7 +165,8 @@ export class DevelopmentRequestController 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 ${data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: data.child1, diff --git a/src/controllers/EmployeePositionController.ts b/src/controllers/EmployeePositionController.ts index 4f679477..8007ac42 100644 --- a/src/controllers/EmployeePositionController.ts +++ b/src/controllers/EmployeePositionController.ts @@ -1154,7 +1154,8 @@ export class EmployeePositionController extends Controller { _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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `posMaster.orgChild1Id is null` : "1=1", { child1: _data.child1, diff --git a/src/controllers/EmployeeTempPositionController.ts b/src/controllers/EmployeeTempPositionController.ts index db110dce..bbb06cea 100644 --- a/src/controllers/EmployeeTempPositionController.ts +++ b/src/controllers/EmployeeTempPositionController.ts @@ -901,7 +901,8 @@ export class EmployeeTempPositionController extends Controller { _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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `posMaster.orgChild1Id is null` : "1=1", { child1: _data.child1, diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 8f5dd6ac..90633992 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -157,8 +157,8 @@ export class OrganizationDotnetController extends Controller { condition = "orgRoot.ancestorDNA = :nodeId"; conditionParams = { nodeId: body.nodeId }; } else if (body.role === "PARENT") { - condition = "orgRoot.ancestorDNA = :nodeId AND current_holders.orgChild1 IS NOT NULL"; - conditionParams = { nodeId: body.nodeId }; + // condition = "orgRoot.ancestorDNA = :nodeId AND current_holders.orgChild1 IS NOT NULL"; + // conditionParams = { nodeId: body.nodeId }; } else if (body.role === "NORMAL") { switch (body.node) { case 0: @@ -298,8 +298,8 @@ export class OrganizationDotnetController extends Controller { condition = "orgRoot.ancestorDNA = :nodeId"; conditionParams = { nodeId: body.nodeId }; } else if (body.role === "PARENT") { - condition = "orgRoot.ancestorDNA = :nodeId AND current_holders.orgChild1 IS NOT NULL"; - conditionParams = { nodeId: body.nodeId }; + // condition = "orgRoot.ancestorDNA = :nodeId AND current_holders.orgChild1 IS NOT NULL"; + // conditionParams = { nodeId: body.nodeId }; } else if (body.role === "NORMAL") { switch (body.node) { case 0: @@ -5743,7 +5743,7 @@ export class OrganizationDotnetController extends Controller { }, ) { let typeCondition: any = {}; - if (body.role === "CHILD" || body.role === "PARENT" || body.role === "BROTHER") { + if (body.role === "CHILD" || /*body.role === "PARENT" ||*/ body.role === "BROTHER") { if (body.role === "CHILD") { switch (body.node) { case 0: @@ -5785,7 +5785,8 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "BROTHER") { + } + else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -5826,15 +5827,16 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "PARENT") { - typeCondition = { - orgRoot: { - ancestorDNA: body.nodeId, - }, - orgChild1: Not(IsNull()), - }; - } - } else if (body.role === "OWNER" || body.role === "ROOT") { + } + // else if (body.role === "PARENT") { + // typeCondition = { + // orgRoot: { + // ancestorDNA: body.nodeId, + // }, + // orgChild1: Not(IsNull()), + // }; + // } + } else if (body.role === "OWNER" || body.role === "ROOT" || body.role === "PARENT") { switch (body.reqNode) { case 0: typeCondition = { @@ -6390,7 +6392,7 @@ export class OrganizationDotnetController extends Controller { }, ) { let typeCondition: any = {}; - if (body.role === "CHILD" || body.role === "PARENT" || body.role === "BROTHER") { + if (body.role === "CHILD" || /*body.role === "PARENT" ||*/ body.role === "BROTHER") { if (body.role === "CHILD") { switch (body.node) { case 0: @@ -6432,7 +6434,8 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "BROTHER") { + } + else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -6473,15 +6476,16 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "PARENT") { - typeCondition = { - orgRoot: { - ancestorDNA: body.nodeId, - }, - orgChild1: Not(IsNull()), - }; - } - } else if (body.role === "OWNER" || body.role === "ROOT") { + } + // else if (body.role === "PARENT") { + // typeCondition = { + // orgRoot: { + // ancestorDNA: body.nodeId, + // }, + // orgChild1: Not(IsNull()), + // }; + // } + } else if (body.role === "OWNER" || body.role === "ROOT" || body.role === "PARENT") { switch (body.reqNode) { case 0: typeCondition = { @@ -7800,7 +7804,7 @@ export class OrganizationDotnetController extends Controller { }, ) { let typeCondition: any = {}; - if (body.role === "CHILD" || body.role === "PARENT" || body.role === "BROTHER") { + if (body.role === "CHILD" || /*body.role === "PARENT" ||*/ body.role === "BROTHER") { if (body.role === "CHILD") { switch (body.node) { case 0: @@ -7832,7 +7836,8 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "BROTHER") { + } + else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -7863,13 +7868,14 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "PARENT") { - typeCondition = { - rootDnaId: body.nodeId, - child1DnaId: Not(IsNull()), - }; - } - } else if (body.role === "OWNER" || body.role === "ROOT") { + } + // else if (body.role === "PARENT") { + // typeCondition = { + // rootDnaId: body.nodeId, + // child1DnaId: Not(IsNull()), + // }; + // } + } else if (body.role === "OWNER" || body.role === "ROOT" || body.role === "PARENT") { switch (body.reqNode) { case 0: typeCondition = { @@ -8025,7 +8031,7 @@ export class OrganizationDotnetController extends Controller { }, ) { let typeCondition: any = {}; - if (body.role === "CHILD" || body.role === "PARENT" || body.role === "BROTHER") { + if (body.role === "CHILD" || /*body.role === "PARENT" ||*/ body.role === "BROTHER") { if (body.role === "CHILD") { switch (body.node) { case 0: @@ -8067,7 +8073,8 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "BROTHER") { + } + else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -8108,15 +8115,16 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "PARENT") { - typeCondition = { - orgRoot: { - ancestorDNA: body.nodeId, - }, - orgChild1: Not(IsNull()), - }; - } - } else if (body.role === "OWNER" || body.role === "ROOT") { + } + // else if (body.role === "PARENT") { + // typeCondition = { + // orgRoot: { + // ancestorDNA: body.nodeId, + // }, + // orgChild1: Not(IsNull()), + // }; + // } + } else if (body.role === "OWNER" || body.role === "ROOT" || body.role === "PARENT") { switch (body.reqNode) { case 0: typeCondition = { @@ -8369,7 +8377,7 @@ export class OrganizationDotnetController extends Controller { }, ) { let typeCondition: any = {}; - if (body.role === "CHILD" || body.role === "PARENT" || body.role === "BROTHER") { + if (body.role === "CHILD" || /*body.role === "PARENT" ||*/ body.role === "BROTHER") { if (body.role === "CHILD") { switch (body.node) { case 0: @@ -8401,7 +8409,8 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "BROTHER") { + } + else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -8432,13 +8441,14 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } else if (body.role === "PARENT") { - typeCondition = { - rootDnaId: body.nodeId, - child1DnaId: Not(IsNull()), - }; - } - } else if (body.role === "OWNER" || body.role === "ROOT") { + } + // else if (body.role === "PARENT") { + // typeCondition = { + // rootDnaId: body.nodeId, + // child1DnaId: Not(IsNull()), + // }; + // } + } else if (body.role === "OWNER" || body.role === "ROOT" || body.role === "PARENT") { switch (body.reqNode) { case 0: typeCondition = { diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index b082f0fd..801d4b97 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -734,8 +734,10 @@ export class PermissionController extends Controller { }; } else if (privilege == "PARENT") { data = { - root: [x.orgRootId], - child1: [null], + // root: [x.orgRootId], + // child1: [null], + root: null, + child1: null, child2: null, child3: null, child4: null, diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 3a42acce..a71e8bbf 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -2390,7 +2390,8 @@ export class PositionController extends Controller { _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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `posMaster.orgChild1Id is null` : "1=1", { child1: _data.child1 } ) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 14a77d7f..8f428180 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -2267,7 +2267,8 @@ export class ProfileController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -6243,7 +6244,8 @@ export class ProfileController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -6630,7 +6632,8 @@ export class ProfileController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -8984,7 +8987,8 @@ export class ProfileController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -9507,7 +9511,8 @@ export class ProfileController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, diff --git a/src/controllers/ProfileEditController.ts b/src/controllers/ProfileEditController.ts index 013a2e9d..a1e081f3 100644 --- a/src/controllers/ProfileEditController.ts +++ b/src/controllers/ProfileEditController.ts @@ -151,7 +151,8 @@ export class ProfileEditController 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 ${data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: data.child1, diff --git a/src/controllers/ProfileEditEmployeeController.ts b/src/controllers/ProfileEditEmployeeController.ts index 86c364d5..87bba20a 100644 --- a/src/controllers/ProfileEditEmployeeController.ts +++ b/src/controllers/ProfileEditEmployeeController.ts @@ -150,7 +150,8 @@ export class ProfileEditEmployeeController 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 ${data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: data.child1, diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 6b2e6688..0d01f7ab 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -2888,7 +2888,8 @@ export class ProfileEmployeeController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -3767,7 +3768,8 @@ export class ProfileEmployeeController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -4325,7 +4327,8 @@ export class ProfileEmployeeController 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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index 6bde1171..99e0754d 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -1105,7 +1105,8 @@ export class ProfileEmployeeTempController extends Controller { _dataOrg.child1 != undefined && _dataOrg.child1 != null ? _dataOrg.child1[0] != null ? `current_holderTemps.orgChild1Id IN (:...child1)` - : `current_holderTemps.orgChild1Id is ${_dataOrg.privilege == "PARENT" ? "not null" : "null"}` + // : `current_holderTemps.orgChild1Id is ${_dataOrg.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _dataOrg.child1, @@ -1560,7 +1561,8 @@ export class ProfileEmployeeTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holderTemps.orgChild1Id IN (:...child1)` - : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + // : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -2271,7 +2273,8 @@ export class ProfileEmployeeTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holderTemps.orgChild1Id IN (:...child1)` - : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + // : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 33d6a835..fc6a9df5 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -133,7 +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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -544,7 +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 ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index fa61df3d..4c3063de 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -96,8 +96,10 @@ class CheckAuth { }; } else if (privilege == "PARENT") { data = { - root: [x.orgRootId], - child1: [null], + // root: [x.orgRootId], + // child1: [null], + root: null, + child1: null, child2: null, child3: null, child4: null, From a80fe85032c375e8be4848d68e9567f0fbd29724 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 19 Feb 2026 15:41:49 +0700 Subject: [PATCH 102/178] fix: bug query --- src/controllers/OrganizationController.ts | 2308 ++++++++++++--------- src/services/OrganizationService.ts | 131 +- 2 files changed, 1461 insertions(+), 978 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index d1b47518..efb0f679 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -51,7 +51,7 @@ import { getRoles, addUserRoles, } from "../keycloak"; -// import { getPositionCounts, getCounts, getRootCounts } from "../services/OrganizationService"; +import { getPositionCountsAggregated, getPositionCount, PositionCountsByNode } from "../services/OrganizationService"; import { BatchSavePosMasterHistoryOfficer, CreatePosMasterHistoryEmployee, @@ -3465,14 +3465,1174 @@ export class OrganizationController extends Controller { return new HttpSuccess(formattedData_); } + // /** + // * API Organizational StructChart + // * + // * @summary Organizational StructChart + // * + // */ + // @Get("struct-chart/{idNode}/{type}") + // async structchart(@Path() idNode: string, type: number) { + // switch (type) { + // case 0: { + // const data = await this.orgRevisionRepository.findOne({ + // where: { id: idNode }, + // relations: [ + // "orgRoots", + // "orgRoots.orgChild1s", + // "orgRoots.orgChild1s.orgChild2s", + // "orgRoots.orgChild1s.orgChild2s.orgChild3s", + // "orgRoots.orgChild1s.orgChild2s.orgChild3s.orgChild4s", + // ], + // }); + // if (!data) { + // throw new HttpError(HttpStatusCode.NOT_FOUND, "not found revision"); + // } + + // const formattedData = { + // departmentName: data.orgRevisionName, + // deptID: data.id, + // type: 0, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgRevisionId: data.id }, + // }), + // totalPositionVacant: + // data.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // data.orgRoots + // .sort((a, b) => a.orgRootOrder - b.orgRootOrder) + // .map(async (orgRoot: OrgRoot) => { + // return { + // departmentName: orgRoot.orgRootName, + // deptID: orgRoot.id, + // type: 1, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgRevisionId: data.id, orgRootId: orgRoot.id }, + // }), + // totalPositionVacant: + // data.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgRoot.orgChild1s + // .sort((a, b) => a.orgChild1Order - b.orgChild1Order) + // .map(async (orgChild1) => ({ + // departmentName: orgChild1.orgChild1Name, + // deptID: orgChild1.id, + // type: 2, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild1.orgChild2s + // .sort((a, b) => a.orgChild2Order - b.orgChild2Order) + // .map(async (orgChild2) => ({ + // departmentName: orgChild2.orgChild2Name, + // deptID: orgChild2.id, + // type: 3, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild2.orgChild3s + // .sort((a, b) => a.orgChild3Order - b.orgChild3Order) + // .map(async (orgChild3) => ({ + // departmentName: orgChild3.orgChild3Name, + // deptID: orgChild3.id, + // type: 4, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: + // // await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count( + // // { + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // next_holderId: IsNull() || "", + // // }, + // // }, + // // ), + // children: await Promise.all( + // orgChild3.orgChild4s + // .sort((a, b) => a.orgChild4Order - b.orgChild4Order) + // .map(async (orgChild4) => ({ + // departmentName: orgChild4.orgChild4Name, + // deptID: orgChild4.id, + // type: 5, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgRootId: orgRoot.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: + // // await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: + // // await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgRootId: orgRoot.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // })), + // ), + // })), + // ), + // })), + // ), + // })), + // ), + // }; + // }), + // ), + // }; + // return new HttpSuccess([formattedData]); + // } + // case 1: { + // const data = await this.orgRootRepository.findOne({ + // where: { id: idNode }, + // relations: [ + // "orgRevision", + // "orgChild1s", + // "orgChild1s.orgChild2s", + // "orgChild1s.orgChild2s.orgChild3s", + // "orgChild1s.orgChild2s.orgChild3s.orgChild4s", + // ], + // }); + // if (!data) { + // throw new HttpError(HttpStatusCode.NOT_FOUND, "not found rootId"); + // } + + // const formattedData = { + // departmentName: data.orgRootName, + // deptID: data.id, + // type: 1, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgRootId: data.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + + // children: await Promise.all( + // data.orgChild1s + // .sort((a, b) => a.orgChild1Order - b.orgChild1Order) + // .map(async (orgChild1) => ({ + // departmentName: orgChild1.orgChild1Name, + // deptID: orgChild1.id, + // type: 2, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgRootId: data.id, orgChild1Id: orgChild1.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild1.orgChild2s + // .sort((a, b) => a.orgChild2Order - b.orgChild2Order) + // .map(async (orgChild2) => ({ + // departmentName: orgChild2.orgChild2Name, + // deptID: orgChild2.id, + // type: 3, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // }, + // }), + // totalPositionCurrentVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild2.orgChild3s + // .sort((a, b) => a.orgChild3Order - b.orgChild3Order) + // .map(async (orgChild3) => ({ + // departmentName: orgChild3.orgChild3Name, + // deptID: orgChild3.id, + // type: 4, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild3.orgChild4s + // .sort((a, b) => a.orgChild4Order - b.orgChild4Order) + // .map(async (orgChild4) => ({ + // departmentName: orgChild4.orgChild4Name, + // deptID: orgChild4.id, + // type: 5, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRootId: data.id, + // orgChild1Id: orgChild1.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: + // // await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRootId: data.id, + // // orgChild1Id: orgChild1.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // })), + // ), + // })), + // ), + // })), + // ), + // })), + // ), + // }; + // return new HttpSuccess([formattedData]); + // } + // case 2: { + // const data = await this.child1Repository.findOne({ + // where: { id: idNode }, + // relations: [ + // "orgRevision", + // "orgChild2s", + // "orgChild2s.orgChild3s", + // "orgChild2s.orgChild3s.orgChild4s", + // ], + // }); + // if (!data) { + // throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child1Id"); + // } + + // const formattedData = { + // departmentName: data.orgChild1Name, + // deptID: data.id, + // type: 2, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgChild1Id: data.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild1Id: data.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild1Id: data.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild1Id: data.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild1Id: data.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + + // children: await Promise.all( + // data.orgChild2s + // .sort((a, b) => a.orgChild2Order - b.orgChild2Order) + // .map(async (orgChild2) => ({ + // departmentName: orgChild2.orgChild2Name, + // deptID: orgChild2.id, + // type: 3, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgChild1Id: data.id, orgChild2Id: orgChild2.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild1Id: data.id, + // orgChild2Id: orgChild2.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild1Id: data.id, + // orgChild2Id: orgChild2.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild1Id: data.id, + // // orgChild2Id: orgChild2.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild1Id: data.id, + // // orgChild2Id: orgChild2.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild2.orgChild3s + // .sort((a, b) => a.orgChild3Order - b.orgChild3Order) + // .map(async (orgChild3) => ({ + // departmentName: orgChild3.orgChild3Name, + // deptID: orgChild3.id, + // type: 4, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild3.orgChild4s + // .sort((a, b) => a.orgChild4Order - b.orgChild4Order) + // .map(async (orgChild4) => ({ + // departmentName: orgChild4.orgChild4Name, + // deptID: orgChild4.id, + // type: 5, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgRevisionId: data.id, + // orgChild2Id: orgChild2.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgRevisionId: data.id, + // // orgChild2Id: orgChild2.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // })), + // ), + // })), + // ), + // })), + // ), + // }; + // return new HttpSuccess([formattedData]); + // } + // case 3: { + // const data = await this.child2Repository.findOne({ + // where: { id: idNode }, + // relations: ["orgRevision", "orgChild3s", "orgChild3s.orgChild4s"], + // }); + // if (!data) { + // throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child2Id"); + // } + + // const formattedData = { + // departmentName: data.orgChild2Name, + // deptID: data.id, + // type: 3, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgChild2Id: data.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild2Id: data.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild2Id: data.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild2Id: data.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild2Id: data.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + + // children: await Promise.all( + // data.orgChild3s + // .sort((a, b) => a.orgChild3Order - b.orgChild3Order) + // .map(async (orgChild3) => ({ + // departmentName: orgChild3.orgChild3Name, + // deptID: orgChild3.id, + // type: 4, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgChild2Id: data.id, orgChild3Id: orgChild3.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild2Id: data.id, + // orgChild3Id: orgChild3.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild2Id: data.id, + // orgChild3Id: orgChild3.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild2Id: data.id, + // // orgChild3Id: orgChild3.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild2Id: data.id, + // // orgChild3Id: orgChild3.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // children: await Promise.all( + // orgChild3.orgChild4s + // .sort((a, b) => a.orgChild4Order - b.orgChild4Order) + // .map(async (orgChild4) => ({ + // departmentName: orgChild4.orgChild4Name, + // deptID: orgChild4.id, + // type: 5, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { + // orgChild2Id: data.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild2Id: data.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild2Id: data.id, + // orgChild3Id: orgChild3.id, + // orgChild4Id: orgChild4.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild2Id: data.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild2Id: data.id, + // // orgChild3Id: orgChild3.id, + // // orgChild4Id: orgChild4.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // })), + // ), + // })), + // ), + // }; + // return new HttpSuccess([formattedData]); + // } + // case 4: { + // const data = await this.child3Repository.findOne({ + // where: { id: idNode }, + // relations: ["orgRevision", "orgChild4s"], + // }); + // if (!data) { + // throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child3Id"); + // } + + // const formattedData = { + // departmentName: data.orgChild3Name, + // deptID: data.id, + // type: 4, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgChild3Id: data.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild3Id: data.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild3Id: data.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild3Id: data.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild3Id: data.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + + // children: await Promise.all( + // data.orgChild4s + // .sort((a, b) => a.orgChild4Order - b.orgChild4Order) + // .map(async (orgChild4) => ({ + // departmentName: orgChild4.orgChild4Name, + // deptID: orgChild4.id, + // type: 5, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgChild3Id: data.id, orgChild4Id: orgChild4.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild3Id: data.id, + // orgChild4Id: orgChild4.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild3Id: data.id, + // orgChild4Id: orgChild4.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild3Id: data.id, + // // orgChild4Id: orgChild4.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild3Id: data.id, + // // orgChild4Id: orgChild4.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // })), + // ), + // }; + // return new HttpSuccess([formattedData]); + // } + // case 5: { + // const data = await this.child4Repository.findOne({ + // where: { id: idNode }, + // relations: ["orgRevision"], + // }); + // if (!data) { + // throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child4Id"); + // } + + // const formattedData = { + // departmentName: data.orgChild4Name, + // deptID: data.id, + // type: 5, + // // heads: + // totalPositionCount: await this.posMasterRepository.count({ + // where: { orgChild4Id: data.id }, + // }), + // totalPositionVacant: + // data.orgRevision.orgRevisionIsDraft == true + // ? await this.posMasterRepository.count({ + // where: { + // orgChild4Id: data.id, + // next_holderId: IsNull() || "", + // }, + // }) + // : await this.posMasterRepository.count({ + // where: { + // orgChild4Id: data.id, + // current_holderId: IsNull() || "", + // }, + // }), + // // totalPositionCurrentVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild4Id: data.id, + // // current_holderId: IsNull() || "", + // // }, + // // }), + // // totalPositionNextVacant: await this.posMasterRepository.count({ + // // where: { + // // orgChild4Id: data.id, + // // next_holderId: IsNull() || "", + // // }, + // // }), + // }; + // return new HttpSuccess([formattedData]); + // } + // default: + // throw new HttpError(HttpStatusCode.NOT_FOUND, "not found type: "); + // } + // } + /** - * API Organizational StructChart + * API Organizational StructChart V2 (Optimized) * - * @summary Organizational StructChart + * @summary Organizational StructChart - Optimized with batch queries to prevent N+1 problem * */ @Get("struct-chart/{idNode}/{type}") - async structchart(@Path() idNode: string, type: number) { + async structchartV2(@Path() idNode: string, type: number) { + // Fetch orgRevisionId and isDraft status first + let orgRevisionId: string | null = null; + let isDraft = false; + + switch (type) { + case 0: + const revision = await this.orgRevisionRepository.findOne({ where: { id: idNode } }); + orgRevisionId = revision?.id || null; + isDraft = revision?.orgRevisionIsDraft || false; + break; + case 1: { + const root = await this.orgRootRepository.findOne({ + where: { id: idNode }, + relations: ["orgRevision"], + }); + orgRevisionId = root?.orgRevision?.id || null; + isDraft = root?.orgRevision?.orgRevisionIsDraft || false; + break; + } + case 2: { + const child1 = await this.child1Repository.findOne({ + where: { id: idNode }, + relations: ["orgRevision"], + }); + orgRevisionId = child1?.orgRevision?.id || null; + isDraft = child1?.orgRevision?.orgRevisionIsDraft || false; + break; + } + case 3: { + const child2 = await this.child2Repository.findOne({ + where: { id: idNode }, + relations: ["orgRevision"], + }); + orgRevisionId = child2?.orgRevision?.id || null; + isDraft = child2?.orgRevision?.orgRevisionIsDraft || false; + break; + } + case 4: { + const child3 = await this.child3Repository.findOne({ + where: { id: idNode }, + relations: ["orgRevision"], + }); + orgRevisionId = child3?.orgRevision?.id || null; + isDraft = child3?.orgRevision?.orgRevisionIsDraft || false; + break; + } + case 5: { + const child4 = await this.child4Repository.findOne({ + where: { id: idNode }, + relations: ["orgRevision"], + }); + orgRevisionId = child4?.orgRevision?.id || null; + isDraft = child4?.orgRevision?.orgRevisionIsDraft || false; + break; + } + default: + throw new HttpError(HttpStatusCode.NOT_FOUND, "not found type: "); + } + + if (!orgRevisionId) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "not found revision"); + } + + // Fetch all position counts in a single batch query (optimized) + const positionCounts = await getPositionCountsAggregated(orgRevisionId); + switch (type) { case 0: { const data = await this.orgRevisionRepository.findOne({ @@ -3493,320 +4653,9 @@ export class OrganizationController extends Controller { departmentName: data.orgRevisionName, deptID: data.id, type: 0, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgRevisionId: data.id }, - }), - totalPositionVacant: - data.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - data.orgRoots - .sort((a, b) => a.orgRootOrder - b.orgRootOrder) - .map(async (orgRoot: OrgRoot) => { - return { - departmentName: orgRoot.orgRootName, - deptID: orgRoot.id, - type: 1, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgRevisionId: data.id, orgRootId: orgRoot.id }, - }), - totalPositionVacant: - data.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgRoot.orgChild1s - .sort((a, b) => a.orgChild1Order - b.orgChild1Order) - .map(async (orgChild1) => ({ - departmentName: orgChild1.orgChild1Name, - deptID: orgChild1.id, - type: 2, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - }, - }), - totalPositionVacant: - data.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild1.orgChild2s - .sort((a, b) => a.orgChild2Order - b.orgChild2Order) - .map(async (orgChild2) => ({ - departmentName: orgChild2.orgChild2Name, - deptID: orgChild2.id, - type: 3, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - }, - }), - totalPositionVacant: - data.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild2.orgChild3s - .sort((a, b) => a.orgChild3Order - b.orgChild3Order) - .map(async (orgChild3) => ({ - departmentName: orgChild3.orgChild3Name, - deptID: orgChild3.id, - type: 4, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - }, - }), - totalPositionVacant: - data.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: - // await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count( - // { - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // next_holderId: IsNull() || "", - // }, - // }, - // ), - children: await Promise.all( - orgChild3.orgChild4s - .sort((a, b) => a.orgChild4Order - b.orgChild4Order) - .map(async (orgChild4) => ({ - departmentName: orgChild4.orgChild4Name, - deptID: orgChild4.id, - type: 5, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - }, - }), - totalPositionVacant: - data.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgRootId: orgRoot.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: - // await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: - // await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgRootId: orgRoot.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // next_holderId: IsNull() || "", - // }, - // }), - })), - ), - })), - ), - })), - ), - })), - ), - }; - }), - ), + totalPositionCount: this.sumAllCounts(positionCounts.orgRootMap), + totalPositionVacant: this.sumAllVacantCounts(positionCounts.orgRootMap, isDraft), + children: this.buildOrgRoots(data.orgRoots, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); } @@ -3825,455 +4674,34 @@ export class OrganizationController extends Controller { throw new HttpError(HttpStatusCode.NOT_FOUND, "not found rootId"); } + const rootCounts = getPositionCount(positionCounts.orgRootMap, data.id); const formattedData = { departmentName: data.orgRootName, deptID: data.id, type: 1, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgRootId: data.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // next_holderId: IsNull() || "", - // }, - // }), - - children: await Promise.all( - data.orgChild1s - .sort((a, b) => a.orgChild1Order - b.orgChild1Order) - .map(async (orgChild1) => ({ - departmentName: orgChild1.orgChild1Name, - deptID: orgChild1.id, - type: 2, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgRootId: data.id, orgChild1Id: orgChild1.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild1.orgChild2s - .sort((a, b) => a.orgChild2Order - b.orgChild2Order) - .map(async (orgChild2) => ({ - departmentName: orgChild2.orgChild2Name, - deptID: orgChild2.id, - type: 3, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - }, - }), - totalPositionCurrentVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild2.orgChild3s - .sort((a, b) => a.orgChild3Order - b.orgChild3Order) - .map(async (orgChild3) => ({ - departmentName: orgChild3.orgChild3Name, - deptID: orgChild3.id, - type: 4, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild3.orgChild4s - .sort((a, b) => a.orgChild4Order - b.orgChild4Order) - .map(async (orgChild4) => ({ - departmentName: orgChild4.orgChild4Name, - deptID: orgChild4.id, - type: 5, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRootId: data.id, - orgChild1Id: orgChild1.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: - // await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRootId: data.id, - // orgChild1Id: orgChild1.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // next_holderId: IsNull() || "", - // }, - // }), - })), - ), - })), - ), - })), - ), - })), - ), + totalPositionCount: rootCounts.totalCount, + totalPositionVacant: isDraft ? rootCounts.nextVacantCount : rootCounts.currentVacantCount, + children: this.buildOrgChild1s(data.orgChild1s, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); } case 2: { const data = await this.child1Repository.findOne({ where: { id: idNode }, - relations: [ - "orgRevision", - "orgChild2s", - "orgChild2s.orgChild3s", - "orgChild2s.orgChild3s.orgChild4s", - ], + relations: ["orgRevision", "orgChild2s", "orgChild2s.orgChild3s", "orgChild2s.orgChild3s.orgChild4s"], }); if (!data) { throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child1Id"); } + const child1Counts = getPositionCount(positionCounts.orgChild1Map, data.id); const formattedData = { departmentName: data.orgChild1Name, deptID: data.id, type: 2, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgChild1Id: data.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild1Id: data.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild1Id: data.id, - // next_holderId: IsNull() || "", - // }, - // }), - - children: await Promise.all( - data.orgChild2s - .sort((a, b) => a.orgChild2Order - b.orgChild2Order) - .map(async (orgChild2) => ({ - departmentName: orgChild2.orgChild2Name, - deptID: orgChild2.id, - type: 3, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgChild1Id: data.id, orgChild2Id: orgChild2.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - orgChild2Id: orgChild2.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild1Id: data.id, - orgChild2Id: orgChild2.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild1Id: data.id, - // orgChild2Id: orgChild2.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild1Id: data.id, - // orgChild2Id: orgChild2.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild2.orgChild3s - .sort((a, b) => a.orgChild3Order - b.orgChild3Order) - .map(async (orgChild3) => ({ - departmentName: orgChild3.orgChild3Name, - deptID: orgChild3.id, - type: 4, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild3.orgChild4s - .sort((a, b) => a.orgChild4Order - b.orgChild4Order) - .map(async (orgChild4) => ({ - departmentName: orgChild4.orgChild4Name, - deptID: orgChild4.id, - type: 5, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgRevisionId: data.id, - orgChild2Id: orgChild2.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgRevisionId: data.id, - // orgChild2Id: orgChild2.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // next_holderId: IsNull() || "", - // }, - // }), - })), - ), - })), - ), - })), - ), + totalPositionCount: child1Counts.totalCount, + totalPositionVacant: isDraft ? child1Counts.nextVacantCount : child1Counts.currentVacantCount, + children: this.buildOrgChild2s(data.orgChild2s, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); } @@ -4286,135 +4714,14 @@ export class OrganizationController extends Controller { throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child2Id"); } + const child2Counts = getPositionCount(positionCounts.orgChild2Map, data.id); const formattedData = { departmentName: data.orgChild2Name, deptID: data.id, type: 3, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgChild2Id: data.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild2Id: data.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild2Id: data.id, - // next_holderId: IsNull() || "", - // }, - // }), - - children: await Promise.all( - data.orgChild3s - .sort((a, b) => a.orgChild3Order - b.orgChild3Order) - .map(async (orgChild3) => ({ - departmentName: orgChild3.orgChild3Name, - deptID: orgChild3.id, - type: 4, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgChild2Id: data.id, orgChild3Id: orgChild3.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild2Id: data.id, - // orgChild3Id: orgChild3.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild2Id: data.id, - // orgChild3Id: orgChild3.id, - // next_holderId: IsNull() || "", - // }, - // }), - children: await Promise.all( - orgChild3.orgChild4s - .sort((a, b) => a.orgChild4Order - b.orgChild4Order) - .map(async (orgChild4) => ({ - departmentName: orgChild4.orgChild4Name, - deptID: orgChild4.id, - type: 5, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild2Id: data.id, - orgChild3Id: orgChild3.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild2Id: data.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild2Id: data.id, - // orgChild3Id: orgChild3.id, - // orgChild4Id: orgChild4.id, - // next_holderId: IsNull() || "", - // }, - // }), - })), - ), - })), - ), + totalPositionCount: child2Counts.totalCount, + totalPositionVacant: isDraft ? child2Counts.nextVacantCount : child2Counts.currentVacantCount, + children: this.buildOrgChild3s(data.orgChild3s, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); } @@ -4427,84 +4734,14 @@ export class OrganizationController extends Controller { throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child3Id"); } + const child3Counts = getPositionCount(positionCounts.orgChild3Map, data.id); const formattedData = { departmentName: data.orgChild3Name, deptID: data.id, type: 4, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgChild3Id: data.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild3Id: data.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild3Id: data.id, - // next_holderId: IsNull() || "", - // }, - // }), - - children: await Promise.all( - data.orgChild4s - .sort((a, b) => a.orgChild4Order - b.orgChild4Order) - .map(async (orgChild4) => ({ - departmentName: orgChild4.orgChild4Name, - deptID: orgChild4.id, - type: 5, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgChild3Id: data.id, orgChild4Id: orgChild4.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - orgChild4Id: orgChild4.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild3Id: data.id, - orgChild4Id: orgChild4.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild3Id: data.id, - // orgChild4Id: orgChild4.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild3Id: data.id, - // orgChild4Id: orgChild4.id, - // next_holderId: IsNull() || "", - // }, - // }), - })), - ), + totalPositionCount: child3Counts.totalCount, + totalPositionVacant: isDraft ? child3Counts.nextVacantCount : child3Counts.currentVacantCount, + children: this.buildOrgChild4s(data.orgChild4s, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); } @@ -4517,40 +4754,13 @@ export class OrganizationController extends Controller { throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child4Id"); } + const child4Counts = getPositionCount(positionCounts.orgChild4Map, data.id); const formattedData = { departmentName: data.orgChild4Name, deptID: data.id, type: 5, - // heads: - totalPositionCount: await this.posMasterRepository.count({ - where: { orgChild4Id: data.id }, - }), - totalPositionVacant: - data.orgRevision.orgRevisionIsDraft == true - ? await this.posMasterRepository.count({ - where: { - orgChild4Id: data.id, - next_holderId: IsNull() || "", - }, - }) - : await this.posMasterRepository.count({ - where: { - orgChild4Id: data.id, - current_holderId: IsNull() || "", - }, - }), - // totalPositionCurrentVacant: await this.posMasterRepository.count({ - // where: { - // orgChild4Id: data.id, - // current_holderId: IsNull() || "", - // }, - // }), - // totalPositionNextVacant: await this.posMasterRepository.count({ - // where: { - // orgChild4Id: data.id, - // next_holderId: IsNull() || "", - // }, - // }), + totalPositionCount: child4Counts.totalCount, + totalPositionVacant: isDraft ? child4Counts.nextVacantCount : child4Counts.currentVacantCount, }; return new HttpSuccess([formattedData]); } @@ -8819,4 +9029,148 @@ export class OrganizationController extends Controller { return { deleted: deletedCount, updated: updatedCount, inserted: insertedCount }; } + + /** + * Helper: Sum all counts from a map (for root level total) + */ + private sumAllCounts(map: Map): number { + let sum = 0; + for (const value of map.values()) { + sum += value.totalCount; + } + return sum; + } + + /** + * Helper: Sum all vacant counts from a map + */ + private sumAllVacantCounts( + map: Map, + isDraft: boolean + ): number { + let sum = 0; + for (const value of map.values()) { + sum += isDraft ? value.nextVacantCount : value.currentVacantCount; + } + return sum; + } + + /** + * Helper: Build orgRoots children array with pre-fetched counts + */ + private buildOrgRoots( + orgRoots: OrgRoot[], + positionCounts: PositionCountsByNode, + isDraft: boolean + ) { + if (!orgRoots) return []; + return orgRoots + .sort((a, b) => a.orgRootOrder - b.orgRootOrder) + .map((orgRoot) => { + const counts = getPositionCount(positionCounts.orgRootMap, orgRoot.id); + return { + departmentName: orgRoot.orgRootName, + deptID: orgRoot.id, + type: 1, + totalPositionCount: counts.totalCount, + totalPositionVacant: isDraft ? counts.nextVacantCount : counts.currentVacantCount, + children: this.buildOrgChild1s(orgRoot.orgChild1s, positionCounts, isDraft), + }; + }); + } + + /** + * Helper: Build orgChild1s children array with pre-fetched counts + */ + private buildOrgChild1s( + orgChild1s: OrgChild1[], + positionCounts: PositionCountsByNode, + isDraft: boolean + ) { + if (!orgChild1s) return []; + return orgChild1s + .sort((a, b) => a.orgChild1Order - b.orgChild1Order) + .map((orgChild1) => { + const counts = getPositionCount(positionCounts.orgChild1Map, orgChild1.id); + return { + departmentName: orgChild1.orgChild1Name, + deptID: orgChild1.id, + type: 2, + totalPositionCount: counts.totalCount, + totalPositionVacant: isDraft ? counts.nextVacantCount : counts.currentVacantCount, + children: this.buildOrgChild2s(orgChild1.orgChild2s, positionCounts, isDraft), + }; + }); + } + + /** + * Helper: Build orgChild2s children array with pre-fetched counts + */ + private buildOrgChild2s( + orgChild2s: OrgChild2[], + positionCounts: PositionCountsByNode, + isDraft: boolean + ) { + if (!orgChild2s) return []; + return orgChild2s + .sort((a, b) => a.orgChild2Order - b.orgChild2Order) + .map((orgChild2) => { + const counts = getPositionCount(positionCounts.orgChild2Map, orgChild2.id); + return { + departmentName: orgChild2.orgChild2Name, + deptID: orgChild2.id, + type: 3, + totalPositionCount: counts.totalCount, + totalPositionVacant: isDraft ? counts.nextVacantCount : counts.currentVacantCount, + children: this.buildOrgChild3s(orgChild2.orgChild3s, positionCounts, isDraft), + }; + }); + } + + /** + * Helper: Build orgChild3s children array with pre-fetched counts + */ + private buildOrgChild3s( + orgChild3s: OrgChild3[], + positionCounts: PositionCountsByNode, + isDraft: boolean + ) { + if (!orgChild3s) return []; + return orgChild3s + .sort((a, b) => a.orgChild3Order - b.orgChild3Order) + .map((orgChild3) => { + const counts = getPositionCount(positionCounts.orgChild3Map, orgChild3.id); + return { + departmentName: orgChild3.orgChild3Name, + deptID: orgChild3.id, + type: 4, + totalPositionCount: counts.totalCount, + totalPositionVacant: isDraft ? counts.nextVacantCount : counts.currentVacantCount, + children: this.buildOrgChild4s(orgChild3.orgChild4s, positionCounts, isDraft), + }; + }); + } + + /** + * Helper: Build orgChild4s children array with pre-fetched counts (leaf nodes) + */ + private buildOrgChild4s( + orgChild4s: OrgChild4[], + positionCounts: PositionCountsByNode, + isDraft: boolean + ) { + if (!orgChild4s) return []; + return orgChild4s + .sort((a, b) => a.orgChild4Order - b.orgChild4Order) + .map((orgChild4) => { + const counts = getPositionCount(positionCounts.orgChild4Map, orgChild4.id); + return { + departmentName: orgChild4.orgChild4Name, + deptID: orgChild4.id, + type: 5, + totalPositionCount: counts.totalCount, + totalPositionVacant: isDraft ? counts.nextVacantCount : counts.currentVacantCount, + }; + }); + } } diff --git a/src/services/OrganizationService.ts b/src/services/OrganizationService.ts index a9b2b796..85cc2220 100644 --- a/src/services/OrganizationService.ts +++ b/src/services/OrganizationService.ts @@ -1,7 +1,121 @@ import { AppDataSource } from "../database/data-source"; import { PosMaster } from "../entities/PosMaster"; -// Helper function to get aggregated position counts +// Type definitions for position counts +export interface PositionCountData { + totalCount: number; + currentVacantCount: number; + nextVacantCount: number; +} + +export interface PositionCountsByNode { + orgRootMap: Map; + orgChild1Map: Map; + orgChild2Map: Map; + orgChild3Map: Map; + orgChild4Map: Map; +} + +// Optimized function using SQL aggregation with GROUP BY +// This is much more efficient than loading all records into memory +export async function getPositionCountsAggregated(orgRevisionId: string): Promise { + const posRepo = AppDataSource.getRepository(PosMaster); + + // Helper to build map from query results + const buildMap = (results: any[]) => { + const map = new Map(); + for (const row of results) { + if (row.nodeId) { + map.set(row.nodeId, { + totalCount: parseInt(row.totalCount) || 0, + currentVacantCount: parseInt(row.currentVacantCount) || 0, + nextVacantCount: parseInt(row.nextVacantCount) || 0, + }); + } + } + return map; + }; + + // Execute all aggregation queries in parallel + const [ + orgRootResults, + orgChild1Results, + orgChild2Results, + orgChild3Results, + orgChild4Results, + ] = await Promise.all([ + // Level 0: orgRoot + posRepo + .createQueryBuilder("pos") + .select("pos.orgRootId", "nodeId") + .addSelect("COUNT(*)", "totalCount") + .addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount") + .addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount") + .where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .andWhere("pos.orgRootId IS NOT NULL") + .groupBy("pos.orgRootId") + .getRawMany(), + + // Level 1: orgChild1 + posRepo + .createQueryBuilder("pos") + .select("pos.orgChild1Id", "nodeId") + .addSelect("COUNT(*)", "totalCount") + .addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount") + .addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount") + .where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .andWhere("pos.orgChild1Id IS NOT NULL") + .groupBy("pos.orgChild1Id") + .getRawMany(), + + // Level 2: orgChild2 + posRepo + .createQueryBuilder("pos") + .select("pos.orgChild2Id", "nodeId") + .addSelect("COUNT(*)", "totalCount") + .addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount") + .addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount") + .where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .andWhere("pos.orgChild2Id IS NOT NULL") + .groupBy("pos.orgChild2Id") + .getRawMany(), + + // Level 3: orgChild3 + posRepo + .createQueryBuilder("pos") + .select("pos.orgChild3Id", "nodeId") + .addSelect("COUNT(*)", "totalCount") + .addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount") + .addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount") + .where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .andWhere("pos.orgChild3Id IS NOT NULL") + .groupBy("pos.orgChild3Id") + .getRawMany(), + + // Level 4: orgChild4 + posRepo + .createQueryBuilder("pos") + .select("pos.orgChild4Id", "nodeId") + .addSelect("COUNT(*)", "totalCount") + .addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount") + .addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount") + .where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId }) + .andWhere("pos.orgChild4Id IS NOT NULL") + .groupBy("pos.orgChild4Id") + .getRawMany(), + ]); + + return { + orgRootMap: buildMap(orgRootResults), + orgChild1Map: buildMap(orgChild1Results), + orgChild2Map: buildMap(orgChild2Results), + orgChild3Map: buildMap(orgChild3Results), + orgChild4Map: buildMap(orgChild4Results), + }; +} + +// Legacy function - kept for backward compatibility +// DEPRECATED: Use getPositionCountsAggregated instead export async function getPositionCounts(orgRevisionId: string) { // Query all posMaster data for this revision with aggregation const rawData = await AppDataSource.getRepository(PosMaster) @@ -276,3 +390,18 @@ export function getRootCounts(map: Map, key: string) { } ); } + +// Helper function to get position counts from aggregated maps with defaults +export function getPositionCount( + map: Map, + key: string | null | undefined +): PositionCountData { + if (!key) return { totalCount: 0, currentVacantCount: 0, nextVacantCount: 0 }; + return ( + map.get(key) || { + totalCount: 0, + currentVacantCount: 0, + nextVacantCount: 0, + } + ); +} From 8497e5df57eaf53394a209b83de71bca24b1583c Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 19 Feb 2026 16:29:36 +0700 Subject: [PATCH 103/178] fix: case clear position all in draft to public to current --- src/controllers/OrganizationController.ts | 72 +++++++++++++++++++---- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index efb0f679..74b90213 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8294,6 +8294,15 @@ export class OrganizationController extends Controller { orgChild4: [...allMappings.orgChild4.byDraftId.keys()], }; + // Get current organization IDs from the mappings (moved up for reuse) + const currentOrgIds = { + orgRoot: [...allMappings.orgRoot.byDraftId.values()], + orgChild1: [...allMappings.orgChild1.byDraftId.values()], + orgChild2: [...allMappings.orgChild2.byDraftId.values()], + orgChild3: [...allMappings.orgChild3.byDraftId.values()], + orgChild4: [...allMappings.orgChild4.byDraftId.values()], + }; + // Get draft positions that belong to any org under the rootDnaId const posMasterDraft = await this.posMasterRepository.find({ where: [ @@ -8305,8 +8314,58 @@ export class OrganizationController extends Controller { ], }); - if (posMasterDraft.length <= 0) - return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง"); + // Special case: If draft has no positions, delete all current positions + if (posMasterDraft.length <= 0) { + // Fetch current positions + const posMasterCurrent = await this.posMasterRepository.find({ + where: [ + { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, + { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, + { orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) }, + { orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) }, + { orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) }, + ], + }); + + if (posMasterCurrent.length > 0) { + const toDeleteIds = posMasterCurrent.map((p) => p.id); + + // Cascade delete positions first + await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); + + // Then delete posMaster records + await queryRunner.manager.delete(PosMaster, toDeleteIds); + + // Save history + const deleteHistoryOps = posMasterCurrent.map((pos) => ({ + posMasterDnaId: pos.ancestorDNA, + profileId: null, + pm: null, + })); + await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); + } + + // Build summary for this special case + const summary = { + message: "ย้ายโครงสร้างสำเร็จ", + organization: { + orgRoot: orgSyncStats.orgRoot, + orgChild1: orgSyncStats.orgChild1, + orgChild2: orgSyncStats.orgChild2, + orgChild3: orgSyncStats.orgChild3, + orgChild4: orgSyncStats.orgChild4, + }, + positionMaster: { + deleted: posMasterCurrent.length, + updated: 0, + inserted: 0, + }, + position: { deleted: 0, updated: 0, inserted: 0 }, + }; + + await queryRunner.commitTransaction(); + return new HttpSuccess(summary); + } // Clear current_holderId for positions that will have new holders const nextHolderIds = posMasterDraft @@ -8344,15 +8403,6 @@ export class OrganizationController extends Controller { } // 2.2 Fetch current positions for comparison - // Get current organization IDs from the mappings - const currentOrgIds = { - orgRoot: [...allMappings.orgRoot.byDraftId.values()], - orgChild1: [...allMappings.orgChild1.byDraftId.values()], - orgChild2: [...allMappings.orgChild2.byDraftId.values()], - orgChild3: [...allMappings.orgChild3.byDraftId.values()], - orgChild4: [...allMappings.orgChild4.byDraftId.values()], - }; - const posMasterCurrent = await this.posMasterRepository.find({ where: [ { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, From 637e99591520b1e2cac5ceb7d852ff6876e8b0e2 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 20 Feb 2026 11:46:46 +0700 Subject: [PATCH 104/178] Fix bug #54 --- src/controllers/OrganizationController.ts | 6 +++--- src/services/ProfileLeaveService.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 74b90213..89015402 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -1663,7 +1663,7 @@ export class OrganizationController extends Controller { // กำหนดการเข้าถึงข้อมูลตามสถานะและสิทธิ์ const isCurrentActive = !orgRevision.orgRevisionIsDraft && orgRevision.orgRevisionIsCurrent; if (isCurrentActive) { - if (profileAssign && _privilege.privilege !== "OWNER") { + if (profileAssign && _privilege.privilege !== "OWNER" && _privilege.privilege !== "PARENT") { if (_privilege.privilege == "NORMAL") { const holder = profile.current_holders.find((x) => x.orgRevisionId === id); if (!holder) return; @@ -5700,7 +5700,7 @@ export class OrganizationController extends Controller { // กำหนดการเข้าถึงข้อมูลตามสถานะและสิทธิ์ const isCurrentActive = !orgRevision.orgRevisionIsDraft && orgRevision.orgRevisionIsCurrent; if (isCurrentActive) { - if (profileAssign && _privilege.privilege !== "OWNER") { + if (profileAssign && _privilege.privilege !== "OWNER" && _privilege.privilege !== "PARENT") { if (_privilege.privilege == "NORMAL") { const holder = profile.current_holders.find((x) => x.orgRevisionId === id); if (!holder) return; @@ -6257,7 +6257,7 @@ export class OrganizationController extends Controller { // กำหนดการเข้าถึงข้อมูลตามสถานะและสิทธิ์ const isCurrentActive = !orgRevision.orgRevisionIsDraft && orgRevision.orgRevisionIsCurrent; if (isCurrentActive) { - if (_privilege.privilege !== "OWNER") { + if (_privilege.privilege !== "OWNER" && _privilege.privilege !== "PARENT") { if (_privilege.privilege == "NORMAL") { const holder = profile.current_holders.find((x) => x.orgRevisionId === id); if (!holder) return; diff --git a/src/services/ProfileLeaveService.ts b/src/services/ProfileLeaveService.ts index 0db2ff19..327a1fe2 100644 --- a/src/services/ProfileLeaveService.ts +++ b/src/services/ProfileLeaveService.ts @@ -289,7 +289,7 @@ export class ProfileLeaveService { isAll?: boolean, ): Promise { // Early return สำหรับ OWNER privilege - if (_data.privilege === "OWNER") { + if (_data.privilege === "OWNER" || _data.privilege === "PARENT") { return { condition: "1=1", params: {} }; } @@ -549,7 +549,7 @@ export class ProfileLeaveService { queryBuilder.andWhere(nodeCondition.condition, nodeCondition.params); - if (_data.privilege !== "OWNER") { + if (_data.privilege !== "OWNER" && _data.privilege !== "PARENT") { queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params); } } @@ -717,7 +717,7 @@ export class ProfileLeaveService { queryBuilder.andWhere(nodeCondition.condition, nodeCondition.params); - if (_data.privilege !== "OWNER") { + if (_data.privilege !== "OWNER" && _data.privilege !== "PARENT") { queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params); } } From 4e396b454d39e450c03250289021a874f2487c80 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 20 Feb 2026 15:17:09 +0700 Subject: [PATCH 105/178] closed#2190 post to exprofile if production only --- src/controllers/CommandController.ts | 530 +++++----- src/controllers/ExRetirementController.ts | 6 + src/controllers/ProfileController.ts | 114 ++- src/controllers/ProfileEmployeeController.ts | 102 +- .../ProfileEmployeeTempController.ts | 918 +++++++++--------- 5 files changed, 831 insertions(+), 839 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 5911940e..579d791d 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -99,7 +99,7 @@ import { CreatePosMasterHistoryOfficer, } from "../services/PositionService"; import { PostRetireToExprofile } from "./ExRetirementController"; -import { LeaveType } from "../entities/LeaveType" +import { LeaveType } from "../entities/LeaveType"; @Route("api/v1/org/command") @Tags("Command") @Security("bearerAuth") @@ -219,8 +219,8 @@ export class CommandController 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, @@ -298,7 +298,7 @@ export class CommandController extends Controller { status == null || status == undefined || status == "" ? null : status.trim().toLocaleUpperCase() == "NEW" || - status.trim().toLocaleUpperCase() == "DRAFT" + status.trim().toLocaleUpperCase() == "DRAFT" ? ["NEW", "DRAFT"] : [status.trim().toLocaleUpperCase()], }, @@ -428,7 +428,7 @@ export class CommandController extends Controller { }); if (profile) { const currentHolder = profile!.current_holders?.find( - x => + (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); @@ -457,29 +457,26 @@ export class CommandController extends Controller { order: { createdAt: "DESC" }, relations: { posExecutive: true }, }); - const operator = Object.assign( - new CommandOperator(), - { - profileId: profile?.id, - prefix: profile?.prefix, - firstName: profile?.firstName, - lastName: profile?.lastName, - posNo: posNo, - posType: profile?.posType?.posTypeName ?? null, - posLevel: profile?.posLevel?.posLevelName ?? null, - position: position?.positionName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - roleName: "เจ้าหน้าที่ดำเนินการ", - orderNo: 1, - commandId: command.id, - createdUserId: request.user.sub, - createdFullName: request.user.name, - createdAt: now, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - lastUpdatedAt: now, - } - ); + const operator = Object.assign(new CommandOperator(), { + profileId: profile?.id, + prefix: profile?.prefix, + firstName: profile?.firstName, + lastName: profile?.lastName, + posNo: posNo, + posType: profile?.posType?.posTypeName ?? null, + posLevel: profile?.posLevel?.posLevelName ?? null, + position: position?.positionName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + roleName: "เจ้าหน้าที่ดำเนินการ", + orderNo: 1, + commandId: command.id, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + }); await this.commandOperatorRepository.save(operator); } } @@ -521,7 +518,7 @@ export class CommandController extends Controller { commandTypeName: command.commandType?.name || null, commandCode: command.commandType?.code || null, commandSysId: command.commandType?.commandSysId || null, - createdUserId: command.createdUserId + createdUserId: command.createdUserId, }; return new HttpSuccess(_command); } @@ -733,7 +730,7 @@ export class CommandController extends Controller { /** * API แก้ไขเงินเดือนทั้งกลุ่ม * - * @summary API แก้ไขเงินเดือนทั้งกลุ่ม + * @summary API แก้ไขเงินเดือนทั้งกลุ่ม * * @param {string} id Id คำสั่ง */ @@ -802,8 +799,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -846,8 +843,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -890,8 +887,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -1175,8 +1172,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); await this.commandReciveRepository.delete({ commandId: command.id }); command.status = "CANCEL"; @@ -1241,8 +1238,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); await this.commandSendCCRepository.delete({ commandSendId: In(commandSend.map((x) => x.id)) }); await this.commandReciveRepository.delete({ commandId: command.id }); @@ -1394,11 +1391,11 @@ export class CommandController extends Controller { let profiles = command && command.commandRecives.length > 0 ? command.commandRecives - .filter((x) => x.profileId != null) - .map((x) => ({ - receiverUserId: x.profileId, - notiLink: "", - })) + .filter((x) => x.profileId != null) + .map((x) => ({ + receiverUserId: x.profileId, + notiLink: "", + })) : []; const msgNoti = { @@ -1430,8 +1427,8 @@ export class CommandController extends Controller { refIds: command.commandRecives.filter((x) => x.refId != null).map((x) => x.refId), status: "WAITING", }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); await this.commandRepository.save(command); } else { const path = commandTypePath(command.commandType.code); @@ -1568,7 +1565,7 @@ export class CommandController extends Controller { ); await this.profileRepository.save(profiles); } - } catch { } + } catch {} type = "EMPLOYEE"; try { @@ -1600,7 +1597,7 @@ export class CommandController extends Controller { ); await this.profileEmployeeRepository.save(profiles); } - } catch { } + } catch {} return new HttpSuccess(); } @@ -1664,7 +1661,7 @@ export class CommandController extends Controller { }), ); } - } catch { } + } catch {} type = "EMPLOYEE"; try { @@ -1719,7 +1716,7 @@ export class CommandController extends Controller { }), ); } - } catch { } + } catch {} return new HttpSuccess(); } @@ -1932,7 +1929,7 @@ export class CommandController extends Controller { .then((x) => { res = x; }) - .catch((x) => { }); + .catch((x) => {}); } let _command; @@ -2010,76 +2007,76 @@ export class CommandController extends Controller { profile?.current_holders.length == 0 ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild4 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4.orgChild4ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild3 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3.orgChild3ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2 != null - ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2.orgChild2ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` - : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgChild1 != null - ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1.orgChild1ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` - : profile?.current_holders.find( (x) => x.orgRevisionId == orgRevisionActive?.id, ) != null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild2 != null + ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2.orgChild2ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` + : profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && profile?.current_holders.find( (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgRoot != null + )?.orgChild1 != null + ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1.orgChild1ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` + : profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + )?.orgRoot != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot.orgRootShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : null; const root = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgRoot; + ?.orgRoot; const child1 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild1; + ?.orgChild1; const child2 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild2; + ?.orgChild2; const child3 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild3; + ?.orgChild3; const child4 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild4; + ?.orgChild4; let _root = root?.orgRootName; let _child1 = child1?.orgChild1Name; @@ -2140,10 +2137,10 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.isLeave == false ? (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root) + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root) : orgLeave : profileTemp.org, fullName: `${x.prefix}${x.firstName} ${x.lastName}`, @@ -2158,8 +2155,8 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.posType && profile?.posLevel ? Extension.ToThaiNumber( - `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, - ) + `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, + ) : "-" : Extension.ToThaiNumber(profileTemp.posLevel), posNo: @@ -2173,19 +2170,19 @@ export class CommandController extends Controller { ? Extension.ToThaiNumber(Extension.ToThaiShortDate_monthYear(profile?.dateRetire)) : profile?.birthDate && commandCode == "C-PM-21" ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear( - new Date( - profile.birthDate.getFullYear() + 60, - profile.birthDate.getMonth(), - profile.birthDate.getDate(), + Extension.ToThaiShortDate_monthYear( + new Date( + profile.birthDate.getFullYear() + 60, + profile.birthDate.getMonth(), + profile.birthDate.getDate(), + ), ), - ), - ) + ) : "-", dateExecute: command.commandExcecuteDate ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), - ) + Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), + ) : "-", remark: x.remarkVertical ? x.remarkVertical : "-", }; @@ -2286,7 +2283,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => { }); + .catch(() => {}); let issue = command.isBangkok == "OFFICE" @@ -2320,7 +2317,7 @@ export class CommandController extends Controller { firstName: true, lastName: true, roleName: true, - orderNo: true + orderNo: true, }, where: { commandId: command.id }, order: { orderNo: "ASC" }, @@ -2341,15 +2338,18 @@ export class CommandController extends Controller { command.commandExcecuteDate == null ? "" : Extension.ToThaiNumber(Extension.ToThaiFullDate2(command.commandExcecuteDate)), - operators: operators.length > 0 - ? operators.map(x => ({ - fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, - roleName: x.roleName - })) - : [{ - fullName: "", - roleName: "เจ้าหน้าที่ดำเนินการ" - }] + operators: + operators.length > 0 + ? operators.map((x) => ({ + fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, + roleName: x.roleName, + })) + : [ + { + fullName: "", + roleName: "เจ้าหน้าที่ดำเนินการ", + }, + ], }, }); } @@ -2594,7 +2594,7 @@ export class CommandController extends Controller { }); if (profile) { const currentHolder = profile!.current_holders?.find( - x => + (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); @@ -2623,29 +2623,26 @@ export class CommandController extends Controller { order: { createdAt: "DESC" }, relations: { posExecutive: true }, }); - const operator = Object.assign( - new CommandOperator(), - { - profileId: profile?.id, - prefix: profile?.prefix, - firstName: profile?.firstName, - lastName: profile?.lastName, - posNo: posNo, - posType: profile?.posType?.posTypeName ?? null, - posLevel: profile?.posLevel?.posLevelName ?? null, - position: position?.positionName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - roleName: "เจ้าหน้าที่ดำเนินการ", - orderNo: 1, - commandId: command.id, - createdUserId: request.user.sub, - createdFullName: request.user.name, - createdAt: now, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - lastUpdatedAt: now, - } - ); + const operator = Object.assign(new CommandOperator(), { + profileId: profile?.id, + prefix: profile?.prefix, + firstName: profile?.firstName, + lastName: profile?.lastName, + posNo: posNo, + posType: profile?.posType?.posTypeName ?? null, + posLevel: profile?.posLevel?.posLevelName ?? null, + position: position?.positionName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + roleName: "เจ้าหน้าที่ดำเนินการ", + orderNo: 1, + commandId: command.id, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + }); await this.commandOperatorRepository.save(operator); } } @@ -2657,8 +2654,8 @@ export class CommandController extends Controller { refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), status: "REPORT", }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); let order = command.commandRecives == null || command.commandRecives.length <= 0 ? 0 @@ -3431,27 +3428,27 @@ export class CommandController extends Controller { ? x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName : x.orgChild3 == null ? x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName - : x.orgChild4 == null - ? x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + "\n" + x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName + : x.orgChild4 == null + ? x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName : x.orgChild4.orgChild4Name + - "\n" + - x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName, + "\n" + + x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName, positionName: x?.current_holder.position ?? _null, profileId: x?.current_holder.id ?? _null, }); @@ -3980,8 +3977,8 @@ export class CommandController extends Controller { relations: { roleKeycloaks: true, posType: true, - posLevel: true - } + posLevel: true, + }, }); if (!profile) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); @@ -4066,7 +4063,7 @@ export class CommandController extends Controller { orgChild2: true, orgChild3: true, orgChild4: true, - } + }, }); orgRootRef = curPosMaster?.orgRoot ?? null; orgChild1Ref = curPosMaster?.orgChild1 ?? null; @@ -4258,28 +4255,17 @@ export class CommandController extends Controller { organizeName = names.join(" "); } await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // item.commandDateAffect?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // profile.posLevel?.posLevelName ?? "", - // item.commandDateAffect ?? new Date(), - // organizeName, - // clearProfile.retireTypeName ?? "", - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", ); } }), @@ -4379,8 +4365,8 @@ export class CommandController extends Controller { relations: { roleKeycloaks: true, posType: true, - posLevel: true - } + posLevel: true, + }, }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); @@ -4466,7 +4452,7 @@ export class CommandController extends Controller { orgChild2: true, orgChild3: true, orgChild4: true, - } + }, }); orgRootRef = curPosMaster?.orgRoot ?? null; orgChild1Ref = curPosMaster?.orgChild1 ?? null; @@ -4509,28 +4495,17 @@ export class CommandController extends Controller { organizeName = names.join(" "); } await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // item.commandDateAffect?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, - // item.commandDateAffect ?? new Date(), - // organizeName, - // clearProfile.retireTypeName ?? "", - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", ); } }), @@ -4633,8 +4608,8 @@ export class CommandController extends Controller { relations: { roleKeycloaks: true, posType: true, - posLevel: true - } + posLevel: true, + }, }); if (!profile) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); @@ -4783,28 +4758,17 @@ export class CommandController extends Controller { organizeName = names.join(" "); } await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // item.commandDateAffect?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // profile.posLevel?.posLevelName ?? "", - // item.commandDateAffect ?? new Date(), - // organizeName, - // clearProfile.retireTypeName ?? "", - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", ); } } @@ -5411,29 +5375,19 @@ export class CommandController extends Controller { let _posLevelName: string = !isEmployee ? `${profile.posLevel?.posLevelName}` : `${profile.posType?.posTypeName} ${profile.posLevel?.posLevelName}`; + await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // item.commandDateAffect?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // _posLevelName, - // item.commandDateAffect ?? new Date(), - // organizeName, - // retireTypeName, - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + _posLevelName, + item.commandDateAffect ?? new Date(), + organizeName, + retireTypeName, ); } }), @@ -5768,14 +5722,14 @@ export class CommandController extends Controller { ) { let _posNumCodeSit: string = ""; let _posNumCodeSitAbb: string = ""; - let commandType: any = "" + let commandType: any = ""; const _command = await this.commandRepository.findOne({ where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, }); if (_command) { commandType = await this.commandTypeRepository.findOne({ select: { code: true }, - where: { id: _command.commandTypeId } + where: { id: _command.commandTypeId }, }); if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { const orgRootDeputy = await this.orgRootRepository.findOne({ @@ -5909,10 +5863,7 @@ export class CommandController extends Controller { if (commandType && String(commandType.code) == "C-PM-11") { const profileIds = body.data.map((x) => x.profileId); - await this.profileRepository.update( - { id: In(profileIds) }, - { isProbation: false } - ); + await this.profileRepository.update({ id: In(profileIds) }, { isProbation: false }); // // Task #2304 อัปเดตจำนวนสิทธิ์การลา เมื่อผ่านทดลองงานฯ // if (leaveType != null) { // await Promise.all( @@ -6071,26 +6022,26 @@ export class CommandController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild4 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; @@ -6187,28 +6138,17 @@ export class CommandController extends Controller { organizeName = names.join(" "); } await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // item.commandDateAffect?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // profile.posLevel?.posLevelName ?? "", - // item.commandDateAffect ?? new Date(), - // organizeName, - // clearProfile.retireTypeName ?? "", - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", ); }), ); @@ -6932,8 +6872,8 @@ export class CommandController extends Controller { prefix: avatar, fileName: fileName, }) - .then(() => { }) - .catch(() => { }); + .then(() => {}) + .catch(() => {}); } } }), @@ -7516,7 +7456,7 @@ export class CommandController extends Controller { where: { profileId: item.posMasterChild.current_holderId, status: true, - isDeleted: false + isDeleted: false, }, }); @@ -8044,7 +7984,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => { }); + .catch(() => {}); let issue = command.isBangkok == "OFFICE" diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index c8174e6f..c64ccdeb 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -183,6 +183,12 @@ export async function PostRetireToExprofile( organizeName: string, // child4Name child3Name child2Name child1Name rootName retireTypeName: string, // เช่น เกษียณ, ขอโอนออก, ลาออก, ปลดออก, ไล่ออก, ... ) { + // check NODE_ENV ถ้าเป็น production ถึงจะทำการส่งข้อมูลไปยัง exprofile + const NODE_ENV = process.env.NODE_ENV || "development"; + if (NODE_ENV !== "production") { + return; + } + let retryCount = 0; const maxRetries = 2; diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 8f428180..2fb7e085 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -322,7 +322,15 @@ export class ProfileController extends Controller { ]; const educations = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + select: [ + "startDate", + "endDate", + "educationLevel", + "degree", + "field", + "institute", + "isDeleted", + ], where: { profileId: id, isDeleted: false }, order: { level: "ASC" }, }); @@ -655,7 +663,15 @@ export class ProfileController extends Controller { ]; const education_raw = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + select: [ + "startDate", + "endDate", + "educationLevel", + "degree", + "field", + "institute", + "isDeleted", + ], where: { profileId: id, isDeleted: false }, // order: { lastUpdatedAt: "DESC" }, order: { level: "ASC" }, @@ -1052,7 +1068,14 @@ export class ProfileController extends Controller { let _child4 = child4?.orgChild4Name; const cert_raw = await this.certificateRepository.find({ - select: ["certificateType", "issuer", "certificateNo", "issueDate", "expireDate", "isDeleted"], + select: [ + "certificateType", + "issuer", + "certificateNo", + "issueDate", + "expireDate", + "isDeleted", + ], where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); @@ -1069,12 +1092,14 @@ export class ProfileController extends Controller { ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) : "", issueToExpireDate: item.issueDate - ? item.expireDate - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.issueDate)} - ${Extension.ToThaiFullDate2(item.expireDate)}`) + ? item.expireDate + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.issueDate)} - ${Extension.ToThaiFullDate2(item.expireDate)}`, + ) : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) : item.expireDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) - : "" + : "", })) : [ { @@ -1246,7 +1271,7 @@ export class ProfileController extends Controller { "page", "refCommandDate", "note", - "isDeleted" + "isDeleted", ], relations: { insignia: { @@ -1325,9 +1350,7 @@ export class ProfileController extends Controller { const totalLeaveDaysKey = `totalLeaveDaysLv${lvIndex}`; const leaveTypeNameKey = `leaveTypeNameLv${lvIndex}`; - const leaveDate = item.maxDateLeaveStart - ? new Date(item.maxDateLeaveStart) - : null; + const leaveDate = item.maxDateLeaveStart ? new Date(item.maxDateLeaveStart) : null; const year = leaveDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(leaveDate)) : ""; @@ -1525,7 +1548,9 @@ export class ProfileController extends Controller { ? actposition_raw.map((item) => ({ date: item.dateStart && item.dateEnd - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`) + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1550,7 +1575,9 @@ export class ProfileController extends Controller { ? assistance_raw.map((item) => ({ date: item.dateStart && item.dateEnd - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`) + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1580,7 +1607,9 @@ export class ProfileController extends Controller { ? duty_raw.map((item) => ({ date: item.dateStart && item.dateEnd - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`) + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1858,7 +1887,7 @@ export class ProfileController extends Controller { ? Extension.ToThaiNumber(profiles.registrationZipCode) : "", fullRegistrationAddress: fullRegistrationAddress, - updateAt: profiles.lastUpdatedAt + updateAt: profiles.lastUpdatedAt ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(profiles.lastUpdatedAt)) : "", telephone: profiles.phone != null ? Extension.ToThaiNumber(profiles.phone) : "", @@ -2267,8 +2296,8 @@ export class ProfileController 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, @@ -6244,8 +6273,8 @@ export class ProfileController 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 }, ) @@ -6632,8 +6661,8 @@ export class ProfileController 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 }, ) @@ -7718,7 +7747,7 @@ export class ProfileController extends Controller { child2Id: null, child3Id: null, child4Id: null, - rootDnaId: null + rootDnaId: null, }); } return new HttpSuccess({ @@ -7728,7 +7757,7 @@ export class ProfileController extends Controller { child2Id: posMasters?.orgChild2Id || null, child3Id: posMasters?.orgChild3Id || null, child4Id: posMasters?.orgChild4Id || null, - rootDnaId: posMasters?.orgRoot?.ancestorDNA || null + rootDnaId: posMasters?.orgRoot?.ancestorDNA || null, }); } @@ -8987,8 +9016,8 @@ export class ProfileController 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 }, ) @@ -9511,8 +9540,8 @@ export class ProfileController 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, @@ -10922,28 +10951,17 @@ export class ProfileController extends Controller { organizeName = names.join(" "); } await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // requestBody.dateLeave?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // profile.posLevel?.posLevelName ?? "", - // requestBody.dateLeave ?? new Date(), - // organizeName, - // "ถึงแก่กรรม", - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + requestBody.dateLeave?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + requestBody.dateLeave ?? new Date(), + organizeName, + "ถึงแก่กรรม", ); return new HttpSuccess(); } diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 0d01f7ab..5fa532ca 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -317,7 +317,15 @@ export class ProfileEmployeeController extends Controller { ]; const educations = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + select: [ + "startDate", + "endDate", + "educationLevel", + "degree", + "field", + "institute", + "isDeleted", + ], where: { profileEmployeeId: id, isDeleted: false }, order: { level: "ASC" }, }); @@ -651,7 +659,15 @@ export class ProfileEmployeeController extends Controller { ]; const education_raw = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + select: [ + "startDate", + "endDate", + "educationLevel", + "degree", + "field", + "institute", + "isDeleted", + ], where: { profileEmployeeId: id, isDeleted: false }, // order: { lastUpdatedAt: "DESC" }, order: { level: "ASC" }, @@ -1048,7 +1064,14 @@ export class ProfileEmployeeController extends Controller { let _child4 = child4?.orgChild4Name; const cert_raw = await this.certificateRepository.find({ - select: ["certificateType", "issuer", "certificateNo", "issueDate", "expireDate", "isDeleted"], + select: [ + "certificateType", + "issuer", + "certificateNo", + "issueDate", + "expireDate", + "isDeleted", + ], where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); @@ -1065,12 +1088,14 @@ export class ProfileEmployeeController extends Controller { ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) : "", issueToExpireDate: item.issueDate - ? item.expireDate - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.issueDate)} - ${Extension.ToThaiFullDate2(item.expireDate)}`) + ? item.expireDate + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.issueDate)} - ${Extension.ToThaiFullDate2(item.expireDate)}`, + ) : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) : item.expireDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) - : "" + : "", })) : [ { @@ -1242,7 +1267,7 @@ export class ProfileEmployeeController extends Controller { "page", "refCommandDate", "note", - "isDeleted" + "isDeleted", ], relations: { insignia: { @@ -1321,9 +1346,7 @@ export class ProfileEmployeeController extends Controller { const totalLeaveDaysKey = `totalLeaveDaysLv${lvIndex}`; const leaveTypeNameKey = `leaveTypeNameLv${lvIndex}`; - const leaveDate = item.maxDateLeaveStart - ? new Date(item.maxDateLeaveStart) - : null; + const leaveDate = item.maxDateLeaveStart ? new Date(item.maxDateLeaveStart) : null; const year = leaveDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(leaveDate)) : ""; @@ -1521,7 +1544,9 @@ export class ProfileEmployeeController extends Controller { ? actposition_raw.map((item) => ({ date: item.dateStart && item.dateEnd - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`) + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1546,7 +1571,9 @@ export class ProfileEmployeeController extends Controller { ? assistance_raw.map((item) => ({ date: item.dateStart && item.dateEnd - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`) + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1576,7 +1603,9 @@ export class ProfileEmployeeController extends Controller { ? duty_raw.map((item) => ({ date: item.dateStart && item.dateEnd - ? Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`) + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1836,7 +1865,7 @@ export class ProfileEmployeeController extends Controller { ? Extension.ToThaiNumber(profiles.registrationZipCode) : "", fullRegistrationAddress: fullRegistrationAddress, - updateAt: profiles.lastUpdatedAt + updateAt: profiles.lastUpdatedAt ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(profiles.lastUpdatedAt)) : "", telephone: profiles.phone != null ? Extension.ToThaiNumber(profiles.phone) : "", @@ -2888,8 +2917,8 @@ export class ProfileEmployeeController 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, @@ -3768,8 +3797,8 @@ export class ProfileEmployeeController 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 }, ) @@ -4327,8 +4356,8 @@ export class ProfileEmployeeController 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, @@ -5396,28 +5425,17 @@ export class ProfileEmployeeController extends Controller { organizeName = names.join(" "); } await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // requestBody.dateLeave?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, - // requestBody.dateLeave ?? new Date(), - // organizeName, - // "ถึงแก่กรรม", - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + requestBody.dateLeave?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, + requestBody.dateLeave ?? new Date(), + organizeName, + "ถึงแก่กรรม", ); return new HttpSuccess(); } diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index 99e0754d..66d66dd7 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -167,7 +167,7 @@ export class ProfileEmployeeTempController extends Controller { }, }); ImgUrl = response_.data.downloadUrl; - } catch { } + } catch {} } const province = await this.provinceRepository.findOneBy({ id: profile.registrationProvinceId, @@ -179,36 +179,36 @@ export class ProfileEmployeeTempController extends Controller { const root = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4; @@ -256,60 +256,68 @@ export class ProfileEmployeeTempController extends Controller { const salarys = salary_raw.length > 1 ? salary_raw.slice(1).map((item) => ({ - date: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiShortDate(item.commandDateAffect)) - : null, - position: Extension.ToThaiNumber( - Extension.ToThaiNumber( - `${item.positionName != null ? item.positionName : "-"} ${item.positionType == null ? item.positionCee ?? "" : (item.positionType == "อำนวยการ" || item.positionType == "บริหาร" ? item.positionType : "") + item.positionLevel}`, + date: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiShortDate(item.commandDateAffect)) + : null, + position: Extension.ToThaiNumber( + Extension.ToThaiNumber( + `${item.positionName != null ? item.positionName : "-"} ${item.positionType == null ? item.positionCee ?? "" : (item.positionType == "อำนวยการ" || item.positionType == "บริหาร" ? item.positionType : "") + item.positionLevel}`, + ), ), - ), - posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : "", - orgRoot: item.orgRoot != null ? Extension.ToThaiNumber(item.orgRoot) : "", - orgChild1: item.orgChild1 != null ? Extension.ToThaiNumber(item.orgChild1) : "", - orgChild2: item.orgChild2 != null ? Extension.ToThaiNumber(item.orgChild2) : "", - orgChild3: item.orgChild3 != null ? Extension.ToThaiNumber(item.orgChild3) : "", - orgChild4: item.orgChild4 != null ? Extension.ToThaiNumber(item.orgChild4) : "", - positionCee: item.positionCee != null ? Extension.ToThaiNumber(item.positionCee) : "", - positionExecutive: - item.positionExecutive != null ? Extension.ToThaiNumber(item.positionExecutive) : "", - })) + posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : "", + orgRoot: item.orgRoot != null ? Extension.ToThaiNumber(item.orgRoot) : "", + orgChild1: item.orgChild1 != null ? Extension.ToThaiNumber(item.orgChild1) : "", + orgChild2: item.orgChild2 != null ? Extension.ToThaiNumber(item.orgChild2) : "", + orgChild3: item.orgChild3 != null ? Extension.ToThaiNumber(item.orgChild3) : "", + orgChild4: item.orgChild4 != null ? Extension.ToThaiNumber(item.orgChild4) : "", + positionCee: item.positionCee != null ? Extension.ToThaiNumber(item.positionCee) : "", + positionExecutive: + item.positionExecutive != null ? Extension.ToThaiNumber(item.positionExecutive) : "", + })) : [ - { - date: "-", - position: "-", - posNo: "-", - orgRoot: null, - orgChild1: null, - orgChild2: null, - orgChild3: null, - orgChild4: null, - positionCee: null, - positionExecutive: null, - }, - ]; + { + date: "-", + position: "-", + posNo: "-", + orgRoot: null, + orgChild1: null, + orgChild2: null, + orgChild3: null, + orgChild4: null, + positionCee: null, + positionExecutive: null, + }, + ]; const educations = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + select: [ + "startDate", + "endDate", + "educationLevel", + "degree", + "field", + "institute", + "isDeleted", + ], where: { profileEmployeeId: id, isDeleted: false }, order: { level: "ASC" }, }); const Education = educations && educations.length > 0 ? educations.map((item) => ({ - institute: item.institute ? item.institute : "-", - date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "-", - degree: item.degree && item.field ? `${item.degree} ${item.field}` : "-", - })) + institute: item.institute ? item.institute : "-", + date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "-", + degree: item.degree && item.field ? `${item.degree} ${item.field}` : "-", + })) : [ - { - institute: "-", - date: "-", - degree: "-", - }, - ]; + { + institute: "-", + date: "-", + degree: "-", + }, + ]; const mapData = { // Id: profile.id, @@ -347,10 +355,10 @@ export class ProfileEmployeeTempController extends Controller { position: salary_raw.length > 0 && salary_raw[0].positionName != null ? Extension.ToThaiNumber( - Extension.ToThaiNumber( - `${salary_raw[0].positionName != null ? salary_raw[0].positionName : "-"} ${salary_raw[0].positionType == null ? salary_raw[0].positionCee ?? "" : (salary_raw[0].positionType == "อำนวยการ" || salary_raw[0].positionType == "บริหาร" ? salary_raw[0].positionType : "") + salary_raw[0].positionLevel}`, - ), - ) + Extension.ToThaiNumber( + `${salary_raw[0].positionName != null ? salary_raw[0].positionName : "-"} ${salary_raw[0].positionType == null ? salary_raw[0].positionCee ?? "" : (salary_raw[0].positionType == "อำนวยการ" || salary_raw[0].positionType == "บริหาร" ? salary_raw[0].positionType : "") + salary_raw[0].positionLevel}`, + ), + ) : "", positionCee: salary_raw.length > 0 && salary_raw[0].positionCee != null @@ -360,22 +368,27 @@ export class ProfileEmployeeTempController extends Controller { salary_raw.length > 0 && salary_raw[0].positionExecutive != null ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].positionExecutive)) : "", - org: `${salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-" - ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild4)) + " " - : "" - }${salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-" + org: `${ + salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-" + ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild4)) + " " + : "" + }${ + salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild3)) + " " : "" - }${salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-" + }${ + salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild2)) + " " : "" - }${salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-" + }${ + salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild1)) + " " : "" - }${salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-" + }${ + salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgRoot)) : "" - }`, + }`, ocFullPath: (_child4 == null ? "" : _child4 + "\n") + (_child3 == null ? "" : _child3 + "\n") + @@ -448,7 +461,7 @@ export class ProfileEmployeeTempController extends Controller { }, }); _ImgUrl[i] = response_.data.downloadUrl; - } catch { } + } catch {} } }), ); @@ -462,7 +475,7 @@ export class ProfileEmployeeTempController extends Controller { }, }); ImgUrl = response_.data.downloadUrl; - } catch { } + } catch {} } const profileOc = await this.profileRepo.findOne({ relations: [ @@ -501,36 +514,36 @@ export class ProfileEmployeeTempController extends Controller { const root = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot; const child1 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1; const child2 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2; const child3 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3; const child4 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4; @@ -549,19 +562,19 @@ export class ProfileEmployeeTempController extends Controller { const certs = cert_raw.length > 0 ? cert_raw.slice(-2).map((item) => ({ - CertificateType: item.certificateType ?? null, - Issuer: item.issuer ?? null, - CertificateNo: Extension.ToThaiNumber(item.certificateNo) ?? null, - IssueDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) ?? null, - })) + CertificateType: item.certificateType ?? null, + Issuer: item.issuer ?? null, + CertificateNo: Extension.ToThaiNumber(item.certificateNo) ?? null, + IssueDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) ?? null, + })) : [ - { - CertificateType: "-", - Issuer: "-", - CertificateNo: "-", - IssueDate: "-", - }, - ]; + { + CertificateType: "-", + Issuer: "-", + CertificateNo: "-", + IssueDate: "-", + }, + ]; const training_raw = await this.trainingRepository.find({ select: ["startDate", "endDate", "place", "department"], where: { profileEmployeeId: id }, @@ -570,34 +583,34 @@ export class ProfileEmployeeTempController extends Controller { const trainings = training_raw.length > 0 ? training_raw.slice(-2).map((item) => ({ - Institute: item.department ?? "", - Start: - item.startDate == null - ? "" - : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)), - End: - item.endDate == null - ? "" - : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)), - Date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "", - Level: "", - Degree: item.name, - Field: "", - })) + Institute: item.department ?? "", + Start: + item.startDate == null + ? "" + : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)), + End: + item.endDate == null + ? "" + : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)), + Date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "", + Level: "", + Degree: item.name, + Field: "", + })) : [ - { - Institute: "-", - Start: "-", - End: "-", - Date: "-", - Level: "-", - Degree: "-", - Field: "-", - }, - ]; + { + Institute: "-", + Start: "-", + End: "-", + Date: "-", + Level: "-", + Degree: "-", + Field: "-", + }, + ]; const discipline_raw = await this.disciplineRepository.find({ select: ["refCommandDate", "refCommandNo", "detail"], @@ -607,22 +620,30 @@ export class ProfileEmployeeTempController extends Controller { const disciplines = discipline_raw.length > 0 ? discipline_raw.slice(-2).map((item) => ({ - DisciplineYear: - Extension.ToThaiNumber(new Date(item.refCommandDate).getFullYear().toString()) ?? - null, - DisciplineDetail: item.detail ?? null, - RefNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, - })) + DisciplineYear: + Extension.ToThaiNumber(new Date(item.refCommandDate).getFullYear().toString()) ?? + null, + DisciplineDetail: item.detail ?? null, + RefNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, + })) : [ - { - DisciplineYear: "-", - DisciplineDetail: "-", - RefNo: "-", - }, - ]; + { + DisciplineYear: "-", + DisciplineDetail: "-", + RefNo: "-", + }, + ]; const education_raw = await this.profileEducationRepo.find({ - select: ["startDate", "endDate", "educationLevel", "degree", "field", "institute", "isDeleted"], + select: [ + "startDate", + "endDate", + "educationLevel", + "degree", + "field", + "institute", + "isDeleted", + ], where: { profileEmployeeId: id, isDeleted: false }, // order: { lastUpdatedAt: "DESC" }, order: { level: "ASC" }, @@ -630,34 +651,34 @@ export class ProfileEmployeeTempController extends Controller { const educations = education_raw.length > 0 ? education_raw.slice(-2).map((item) => ({ - Institute: item.institute, - Start: - item.startDate == null - ? "" - : Extension.ToThaiNumber(new Date(item.startDate).getFullYear().toString()), - End: - item.endDate == null - ? "" - : Extension.ToThaiNumber(new Date(item.endDate).getFullYear().toString()), - Date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "", - Level: item.educationLevel ?? "", - Degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", - Field: item.field ?? "-", - })) + Institute: item.institute, + Start: + item.startDate == null + ? "" + : Extension.ToThaiNumber(new Date(item.startDate).getFullYear().toString()), + End: + item.endDate == null + ? "" + : Extension.ToThaiNumber(new Date(item.endDate).getFullYear().toString()), + Date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "", + Level: item.educationLevel ?? "", + Degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", + Field: item.field ?? "-", + })) : [ - { - Institute: "-", - Start: "-", - End: "-", - Date: "-", - Level: "-", - Degree: "-", - Field: "-", - }, - ]; + { + Institute: "-", + Start: "-", + End: "-", + Date: "-", + Level: "-", + Degree: "-", + Field: "-", + }, + ]; const salary_raw = await this.salaryRepo.find({ select: [ "commandDateAffect", @@ -678,50 +699,50 @@ export class ProfileEmployeeTempController extends Controller { const salarys = salary_raw.length > 0 ? salary_raw.map((item) => ({ - SalaryDate: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : null, - Position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, - PosNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : null, - Salary: - item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, - Special: - item.amountSpecial != null - ? Extension.ToThaiNumber(item.amountSpecial.toLocaleString()) + SalaryDate: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) : null, - Rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, - RefAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, - PositionLevel: - item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, - PositionType: item.positionType ?? null, - PositionAmount: - item.positionSalaryAmount == null - ? null - : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), - FullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, - OcFullPath: - (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root), - })) + Position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, + PosNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : null, + Salary: + item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, + Special: + item.amountSpecial != null + ? Extension.ToThaiNumber(item.amountSpecial.toLocaleString()) + : null, + Rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, + RefAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, + PositionLevel: + item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, + PositionType: item.positionType ?? null, + PositionAmount: + item.positionSalaryAmount == null + ? null + : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), + FullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, + OcFullPath: + (_child4 == null ? "" : _child4 + "\n") + + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root), + })) : [ - { - SalaryDate: "-", - Position: "-", - PosNo: "-", - Salary: "-", - Special: "-", - Rank: "-", - RefAll: "-", - PositionLevel: "-", - PositionType: "-", - PositionAmount: "-", - FullName: "-", - OcFullPath: "-", - }, - ]; + { + SalaryDate: "-", + Position: "-", + PosNo: "-", + Salary: "-", + Special: "-", + Rank: "-", + RefAll: "-", + PositionLevel: "-", + PositionType: "-", + PositionAmount: "-", + FullName: "-", + OcFullPath: "-", + }, + ]; const insignia_raw = await this.profileInsigniaRepo.find({ relations: { @@ -735,37 +756,37 @@ export class ProfileEmployeeTempController extends Controller { const insignias = insignia_raw.length > 0 ? insignia_raw.map((item) => ({ - ReceiveDate: item.receiveDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) - : "", - InsigniaName: item.insignia.name, - InsigniaShortName: item.insignia.shortName, - InsigniaTypeName: item.insignia.insigniaType.name, - No: item.no ? Extension.ToThaiNumber(item.no) : "", - Issue: item.issue ? item.issue : "", - VolumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", - Volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", - Section: item.section ? Extension.ToThaiNumber(item.section) : "", - Page: item.page ? Extension.ToThaiNumber(item.page) : "", - RefCommandDate: item.refCommandDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) - : "", - })) + ReceiveDate: item.receiveDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) + : "", + InsigniaName: item.insignia.name, + InsigniaShortName: item.insignia.shortName, + InsigniaTypeName: item.insignia.insigniaType.name, + No: item.no ? Extension.ToThaiNumber(item.no) : "", + Issue: item.issue ? item.issue : "", + VolumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", + Volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", + Section: item.section ? Extension.ToThaiNumber(item.section) : "", + Page: item.page ? Extension.ToThaiNumber(item.page) : "", + RefCommandDate: item.refCommandDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) + : "", + })) : [ - { - ReceiveDate: "-", - InsigniaName: "-", - InsigniaShortName: "-", - InsigniaTypeName: "-", - No: "-", - Issue: "-", - VolumeNo: "-", - Volume: "-", - Section: "-", - Page: "-", - RefCommandDate: "-", - }, - ]; + { + ReceiveDate: "-", + InsigniaName: "-", + InsigniaShortName: "-", + InsigniaTypeName: "-", + No: "-", + Issue: "-", + VolumeNo: "-", + Volume: "-", + Section: "-", + Page: "-", + RefCommandDate: "-", + }, + ]; const leave_raw = await this.profileLeaveRepository.find({ relations: { leaveType: true }, @@ -775,19 +796,19 @@ export class ProfileEmployeeTempController extends Controller { const leaves = leave_raw.length > 0 ? leave_raw.map((item) => ({ - LeaveTypeName: item.leaveType.name, - DateLeaveStart: item.dateLeaveStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) - : "", - LeaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "", - })) + LeaveTypeName: item.leaveType.name, + DateLeaveStart: item.dateLeaveStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + : "", + LeaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "", + })) : [ - { - LeaveTypeName: "-", - DateLeaveStart: "-", - LeaveDays: "-", - }, - ]; + { + LeaveTypeName: "-", + DateLeaveStart: "-", + LeaveDays: "-", + }, + ]; const data = { fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, @@ -814,20 +835,20 @@ export class ProfileEmployeeTempController extends Controller { profiles.citizenId != null ? Extension.ToThaiNumber(profiles.citizenId.toString()) : "", fatherFullName: profileFamilyFather?.fatherPrefix || - profileFamilyFather?.fatherFirstName || - profileFamilyFather?.fatherLastName + profileFamilyFather?.fatherFirstName || + profileFamilyFather?.fatherLastName ? `${profileFamilyFather?.fatherPrefix ?? ""}${profileFamilyFather?.fatherFirstName ?? ""} ${profileFamilyFather?.fatherLastName ?? ""}`.trim() : null, motherFullName: profileFamilyMother?.motherPrefix || - profileFamilyMother?.motherFirstName || - profileFamilyMother?.motherLastName + profileFamilyMother?.motherFirstName || + profileFamilyMother?.motherLastName ? `${profileFamilyMother?.motherPrefix ?? ""}${profileFamilyMother?.motherFirstName ?? ""} ${profileFamilyMother?.motherLastName ?? ""}`.trim() : null, coupleFullName: profileFamilyCouple?.couplePrefix || - profileFamilyCouple?.coupleFirstName || - profileFamilyCouple?.coupleLastNameOld + profileFamilyCouple?.coupleFirstName || + profileFamilyCouple?.coupleLastNameOld ? `${profileFamilyCouple?.couplePrefix ?? ""}${profileFamilyCouple?.coupleFirstName ?? ""} ${profileFamilyCouple?.coupleLastName ?? ""}`.trim() : null, coupleLastNameOld: profileFamilyCouple?.coupleLastNameOld ?? null, @@ -1105,8 +1126,8 @@ export class ProfileEmployeeTempController extends Controller { _dataOrg.child1 != undefined && _dataOrg.child1 != null ? _dataOrg.child1[0] != null ? `current_holderTemps.orgChild1Id IN (:...child1)` - // : `current_holderTemps.orgChild1Id is ${_dataOrg.privilege == "PARENT" ? "not null" : "null"}` - : `current_holders.orgChild1Id is null` + : // : `current_holderTemps.orgChild1Id is ${_dataOrg.privilege == "PARENT" ? "not null" : "null"}` + `current_holders.orgChild1Id is null` : "1=1", { child1: _dataOrg.child1, @@ -1176,8 +1197,8 @@ export class ProfileEmployeeTempController extends Controller { _data.profileEmployeeEmployment.length == 0 ? null : _data.profileEmployeeEmployment.reduce((latest, current) => { - return latest.date > current.date ? latest : current; - }).date; + return latest.date > current.date ? latest : current; + }).date; return { id: _data.id, prefix: _data.prefix, @@ -1327,32 +1348,32 @@ export class ProfileEmployeeTempController extends Controller { profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = profile.current_holders.length == 0 || - (profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) + (profile.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : profile.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; @@ -1561,8 +1582,8 @@ export class ProfileEmployeeTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holderTemps.orgChild1Id IN (:...child1)` - // : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `current_holders.orgChild1Id is null` + : // : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -1673,35 +1694,35 @@ export class ProfileEmployeeTempController extends Controller { _data.current_holderTemps.length == 0 ? null : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild4 != null + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild4 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild3 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + null && + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + null && + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + _data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${_data.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const dateEmployment = _data.profileEmployeeEmployment.length == 0 ? null : _data.profileEmployeeEmployment.reduce((latest, current) => { - return latest.date > current.date ? latest : current; - }).date; + return latest.date > current.date ? latest : current; + }).date; return { id: _data.id, prefix: _data.prefix, @@ -1952,8 +1973,8 @@ export class ProfileEmployeeTempController extends Controller { .map((x) => x.current_holderId).length == 0 ? ["zxc"] : orgRevision.employeeTempPosMasters - .filter((x) => x.current_holderId != null) - .map((x) => x.current_holderId), + .filter((x) => x.current_holderId != null) + .map((x) => x.current_holderId), }); }), ) @@ -2122,74 +2143,74 @@ export class ProfileEmployeeTempController extends Controller { posTypeId: profile.posType == null ? null : profile.posType.id, rootId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRootId, + ?.orgRootId, root: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot - .orgRootName, + .orgRootName, child1Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild1Id, + ?.orgChild1Id, child1: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 - .orgChild1Name, + .orgChild1Name, child2Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild2Id, + ?.orgChild2Id, child2: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 - .orgChild2Name, + .orgChild2Name, child3Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild3Id, + ?.orgChild3Id, child3: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 - .orgChild3Name, + .orgChild3Name, child4Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild4Id, + ?.orgChild4Id, child4: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 - .orgChild4Name, + .orgChild4Name, salary: profile ? profile.amount : null, amountSpecial: profile ? profile.amountSpecial : null, }; @@ -2273,8 +2294,8 @@ export class ProfileEmployeeTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holderTemps.orgChild1Id IN (:...child1)` - // : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `current_holders.orgChild1Id is null` + : // : `current_holderTemps.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -2318,34 +2339,34 @@ export class ProfileEmployeeTempController extends Controller { item.current_holderTemps.length == 0 ? null : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild4 != null + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild4 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild3 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = item.current_holderTemps.length == 0 || - (item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == + (item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id) != null && + item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : item.current_holderTemps.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; @@ -2831,54 +2852,54 @@ export class ProfileEmployeeTempController extends Controller { isProbation: item.isProbation, orgRootName: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot + ?.orgRootName == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName, + ?.orgRootName, orgChild1Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 - ?.orgChild1Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 + ?.orgChild1Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild1?.orgChild1Name, + ?.orgChild1?.orgChild1Name, orgChild2Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 - ?.orgChild2Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 + ?.orgChild2Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2?.orgChild2Name, + ?.orgChild2?.orgChild2Name, orgChild3Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 - ?.orgChild3Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 + ?.orgChild3Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3?.orgChild3Name, + ?.orgChild3?.orgChild3Name, orgChild4Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 - ?.orgChild4Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 + ?.orgChild4Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4?.orgChild4Name, + ?.orgChild4?.orgChild4Name, }; }), ); @@ -3097,7 +3118,7 @@ export class ProfileEmployeeTempController extends Controller { isLeave: false, isRetired: item.current_holder.birthDate == null || - calculateRetireDate(item.current_holder.birthDate).getFullYear() != body.year + calculateRetireDate(item.current_holder.birthDate).getFullYear() != body.year ? false : true, isSpecial: false, @@ -3153,68 +3174,68 @@ export class ProfileEmployeeTempController extends Controller { posTypeId: profile.posType == null ? null : profile.posType.id, rootId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRootId, root: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot.orgRootName, child1Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1Id, child1: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 - .orgChild1Name, + .orgChild1Name, child2Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2Id, child2: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 - .orgChild2Name, + .orgChild2Name, child3Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3Id, child3: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 - .orgChild3Name, + .orgChild3Name, child4Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4Id, child4: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 - .orgChild4Name, + .orgChild4Name, }; return new HttpSuccess(_profile); } @@ -3267,8 +3288,8 @@ export class ProfileEmployeeTempController extends Controller { const formattedData = profiles.map((item) => { const posMaster = item.current_holders == null || - item.current_holders.length == 0 || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null + item.current_holders.length == 0 || + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id); // const posExecutive = @@ -3293,49 +3314,49 @@ export class ProfileEmployeeTempController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; const child1 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1; const child2 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2; const child3 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3; const child4 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4; @@ -3459,24 +3480,24 @@ export class ProfileEmployeeTempController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; const dest_item = await this.salaryRepo.findOne({ @@ -3560,28 +3581,17 @@ export class ProfileEmployeeTempController extends Controller { organizeName = names.join(" "); } await PostRetireToExprofile( - // profile.citizenId ?? "", - // profile.prefix ?? "", - // profile.firstName ?? "", - // profile.lastName ?? "", - // requestBody.dateLeave?.getFullYear().toString() ?? "", - // profile.position, - // profile.posType?.posTypeName ?? "", - // `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, - // requestBody.dateLeave ?? new Date(), - // organizeName, - // "ถึงแก่กรรม", - "310190004095X", - "จ.ส.อ.", - "dev", - "hrms", - "2026", - "เจ้าหน้าที่จัดเก็บรายได้", - "อื่นๆ", - "C 3", - new Date(2026, 0, 1), - "สำนักงานเขตบางกอกใหญ่", - "เกษียณ" + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + requestBody.dateLeave?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, + requestBody.dateLeave ?? new Date(), + organizeName, + "ถึงแก่กรรม", ); return new HttpSuccess(); } @@ -3918,7 +3928,7 @@ export class ProfileEmployeeTempController extends Controller { positionId: profile.positionIdTemp, profileId: profile.id, }) - .then(async () => { }); + .then(async () => {}); } }), ); @@ -4009,32 +4019,32 @@ export class ProfileEmployeeTempController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = item.current_holders.length == 0 || - (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) + (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; @@ -4127,36 +4137,36 @@ export class ProfileEmployeeTempController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -4170,27 +4180,27 @@ export class ProfileEmployeeTempController extends Controller { profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild4 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild4 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild2 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : null; const _profile: any = { From 673da9940db35736eb9525c5be988997a890c284 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 23 Feb 2026 09:22:27 +0700 Subject: [PATCH 106/178] =?UTF-8?q?Insert=20=E0=B8=9B=E0=B8=A3=E0=B8=B0?= =?UTF-8?q?=E0=B8=A7=E0=B8=B1=E0=B8=95=E0=B8=B4=E0=B9=81=E0=B8=81=E0=B9=89?= =?UTF-8?q?=E0=B9=84=E0=B8=82=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=B9?= =?UTF-8?q?=E0=B8=A5=E0=B8=AA=E0=B9=88=E0=B8=A7=E0=B8=99=E0=B8=95=E0=B8=B1?= =?UTF-8?q?=E0=B8=A7=20=E0=B8=81=E0=B8=A3=E0=B8=93=E0=B8=B5=E0=B9=81?= =?UTF-8?q?=E0=B8=81=E0=B9=89=E0=B9=84=E0=B8=82=E0=B8=84=E0=B8=A3=E0=B8=B1?= =?UTF-8?q?=E0=B9=89=E0=B8=87=E0=B9=81=E0=B8=A3=E0=B8=81=20Task=20#2314?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileController.ts | 70 +++++++++++++------ src/controllers/ProfileEmployeeController.ts | 70 +++++++++++++------ .../ProfileEmployeeTempController.ts | 70 +++++++++++++------ 3 files changed, 144 insertions(+), 66 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 2fb7e085..483c580d 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -5506,20 +5506,33 @@ export class ProfileController extends Controller { */ @Get("history/user") async getHistoryProfileByUser(@Request() request: RequestWithUser) { - const historyProfile = await this.profileHistoryRepo.find({ - relations: { - posLevel: true, - posType: true, - }, - where: { keycloak: request.user.sub }, - order: { - createdAt: "ASC", - }, + const profile = await this.profileRepo.findOne({ + where: { keycloak: request.user.sub } }); + if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - if (!historyProfile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const profileHistory = await this.profileHistoryRepo.find({ + where: { profileId: profile.id }, + order: { createdAt: "ASC" } + }); + + if (profileHistory.length == 0) { + await this.profileHistoryRepo.save( + Object.assign(new ProfileHistory(), { + ...profile, + birthDateOld: profile?.birthDate, + profileId: profile.id, + id: undefined + }), + ); + const firstRecord = await this.profileHistoryRepo.find({ + where: { profileId: profile.id }, + order: { createdAt: "ASC" } + }); + return new HttpSuccess(firstRecord); + } - return new HttpSuccess(historyProfile); + return new HttpSuccess(profileHistory); } /** @@ -6468,20 +6481,33 @@ export class ProfileController extends Controller { @Get("history/{id}") async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) { //await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", id); //ไม่แน่ใจOFFปิดไว้ก่อน - const profile = await this.profileHistoryRepo.find({ - relations: { - posLevel: true, - posType: true, - }, - where: { profileId: id }, - order: { - createdAt: "ASC", - }, + const profile = await this.profileRepo.findOne({ + where: { id: id } }); - if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - return new HttpSuccess(profile); + const profileHistory = await this.profileHistoryRepo.find({ + where: { profileId: id }, + order: { createdAt: "ASC" } + }); + + if (profileHistory.length == 0) { + await this.profileHistoryRepo.save( + Object.assign(new ProfileHistory(), { + ...profile, + birthDateOld: profile?.birthDate, + profileId: id, + id: undefined + }), + ); + const firstRecord = await this.profileHistoryRepo.find({ + where: { profileId: id }, + order: { createdAt: "ASC" } + }); + return new HttpSuccess(firstRecord); + } + + return new HttpSuccess(profileHistory); } /** diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 5fa532ca..72835a48 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -2289,20 +2289,33 @@ export class ProfileEmployeeController extends Controller { */ @Get("history/user") async getHistoryProfileByUser(@Request() request: RequestWithUser) { - const historyProfile = await this.profileHistoryRepo.find({ - relations: { - posLevel: true, - posType: true, - }, - where: { keycloak: request.user.sub }, - order: { - createdAt: "ASC", - }, + const profile = await this.profileRepo.findOne({ + where: { keycloak: request.user.sub } }); + if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - if (!historyProfile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const profileHistory = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: profile.id }, + order: { createdAt: "ASC" } + }); + + if (profileHistory.length == 0) { + await this.profileHistoryRepo.save( + Object.assign(new ProfileEmployeeHistory(), { + ...profile, + birthDateOld: profile?.birthDate, + profileEmployeeId: profile.id, + id: undefined + }), + ); + const firstRecord = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: profile.id }, + order: { createdAt: "ASC" } + }); + return new HttpSuccess(firstRecord); + } - return new HttpSuccess(historyProfile); + return new HttpSuccess(profileHistory); } /** @@ -3193,20 +3206,33 @@ export class ProfileEmployeeController extends Controller { @Get("history/{id}") async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) { //await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", id); ไม่แน่ใจEMPปิดไว้ก่อน; - const profile = await this.profileHistoryRepo.find({ - relations: { - posLevel: true, - posType: true, - }, - where: { profileEmployeeId: id }, - order: { - createdAt: "ASC", - }, + const profile = await this.profileRepo.findOne({ + where: { id: id } }); - if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - return new HttpSuccess(profile); + const profileHistory = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: id }, + order: { createdAt: "ASC" } + }); + + if (profileHistory.length == 0) { + await this.profileHistoryRepo.save( + Object.assign(new ProfileEmployeeHistory(), { + ...profile, + birthDateOld: profile?.birthDate, + profileEmployeeId: id, + id: undefined + }), + ); + const firstRecord = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: id }, + order: { createdAt: "ASC" } + }); + return new HttpSuccess(firstRecord); + } + + return new HttpSuccess(profileHistory); } /** diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index 66d66dd7..72dd1c43 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -1294,20 +1294,33 @@ export class ProfileEmployeeTempController extends Controller { */ @Get("history/user") async getHistoryProfileByUser(@Request() request: RequestWithUser) { - const historyProfile = await this.profileHistoryRepo.find({ - relations: { - posLevel: true, - posType: true, - }, - where: { keycloak: request.user.sub }, - order: { - createdAt: "ASC", - }, + const profile = await this.profileRepo.findOne({ + where: { keycloak: request.user.sub } }); + if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - if (!historyProfile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const profileHistory = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: profile.id }, + order: { createdAt: "ASC" } + }); + + if (profileHistory.length == 0) { + await this.profileHistoryRepo.save( + Object.assign(new ProfileEmployeeHistory(), { + ...profile, + birthDateOld: profile?.birthDate, + profileEmployeeId: profile.id, + id: undefined + }), + ); + const firstRecord = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: profile.id }, + order: { createdAt: "ASC" } + }); + return new HttpSuccess(firstRecord); + } - return new HttpSuccess(historyProfile); + return new HttpSuccess(profileHistory); } /** @@ -1814,20 +1827,33 @@ export class ProfileEmployeeTempController extends Controller { @Get("history/{id}") async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) { // await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP");//ไม่แน่ใจTEMPปิดไว้ก่อน - const profile = await this.profileHistoryRepo.find({ - relations: { - posLevel: true, - posType: true, - }, - where: { profileEmployeeId: id }, - order: { - createdAt: "ASC", - }, + const profile = await this.profileRepo.findOne({ + where: { id: id } }); - if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - return new HttpSuccess(profile); + const profileHistory = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: id }, + order: { createdAt: "ASC" } + }); + + if (profileHistory.length == 0) { + await this.profileHistoryRepo.save( + Object.assign(new ProfileEmployeeHistory(), { + ...profile, + birthDateOld: profile?.birthDate, + profileEmployeeId: id, + id: undefined + }), + ); + const firstRecord = await this.profileHistoryRepo.find({ + where: { profileEmployeeId: id }, + order: { createdAt: "ASC" } + }); + return new HttpSuccess(firstRecord); + } + + return new HttpSuccess(profileHistory); } /** From f6c726baa5dd02594bf9f4379d0d284621d068a0 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 23 Feb 2026 15:06:32 +0700 Subject: [PATCH 107/178] =?UTF-8?q?Migrate=20=E0=B8=AD=E0=B8=B1=E0=B8=95?= =?UTF-8?q?=E0=B8=A3=E0=B8=B2=E0=B8=81=E0=B8=B3=E0=B8=A5=E0=B8=B1=E0=B8=87?= =?UTF-8?q?=E0=B8=A5=E0=B8=B9=E0=B8=81=E0=B8=88=E0=B9=89=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B8=88=E0=B8=B3=20=E0=B9=80?= =?UTF-8?q?=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=A1=E0=B9=80=E0=B8=A1=E0=B8=99?= =?UTF-8?q?=E0=B8=B9=E0=B8=84=E0=B8=B1=E0=B8=94=E0=B8=A5=E0=B8=AD=E0=B8=81?= =?UTF-8?q?/=E0=B8=88=E0=B8=B1=E0=B8=94=E0=B8=81=E0=B8=B2=E0=B8=A3?= =?UTF-8?q?=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=87?= =?UTF-8?q?=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B9=80=E0=B8=87=E0=B8=B7=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=E0=B9=84=E0=B8=82=20#2316=20&&=20Fix=20Bug?= =?UTF-8?q?=20=E0=B8=81=E0=B8=94=E0=B8=A5=E0=B8=9A=E0=B8=96=E0=B8=B2?= =?UTF-8?q?=E0=B8=A7=E0=B8=A3=E0=B8=84=E0=B8=B3=E0=B8=AA=E0=B8=B1=E0=B9=88?= =?UTF-8?q?=E0=B8=87=E0=B9=81=E0=B8=A5=E0=B9=89=E0=B8=A7=20error=20#216?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 1 + src/controllers/EmployeePositionController.ts | 31 +++++++++++++++++++ src/entities/EmployeePosMaster.ts | 14 +++++++++ ...te_empPosMaster_add_fields_isCondition_.ts | 20 ++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 src/migration/1771832659308-update_empPosMaster_add_fields_isCondition_.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 579d791d..d9428e27 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -1244,6 +1244,7 @@ export class CommandController extends Controller { await this.commandSendCCRepository.delete({ commandSendId: In(commandSend.map((x) => x.id)) }); await this.commandReciveRepository.delete({ commandId: command.id }); await this.commandSendRepository.delete({ commandId: command.id }); + await this.commandOperatorRepository.delete({ commandId: command.id }); await this.commandRepository.delete(command.id); return new HttpSuccess(); } diff --git a/src/controllers/EmployeePositionController.ts b/src/controllers/EmployeePositionController.ts index 8007ac42..9baec79b 100644 --- a/src/controllers/EmployeePositionController.ts +++ b/src/controllers/EmployeePositionController.ts @@ -940,6 +940,35 @@ export class EmployeePositionController extends Controller { return new HttpSuccess(posMaster.id); } + /** + * @summary แก้ไขตำแหน่งเงื่อนไข ลูกจ้างประจำ (ADMIN) + */ + @Put("master/position-condition/{id}") + async updatePositionCondition( + @Path() id: string, + @Body() + requestBody: { + isCondition: boolean | null; + conditionReason: string | null; + }, + @Request() request: RequestWithUser, + ) { + await new permission().PermissionUpdate(request, "SYS_POS_CONDITION"); + const posMaster = await this.employeePosMasterRepository.findOne({ + where: { id: id }, + }); + if (!posMaster) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล"); + } + + Object.assign(posMaster, requestBody); + posMaster.lastUpdateUserId = request.user.sub; + posMaster.lastUpdateFullName = request.user.name; + posMaster.lastUpdatedAt = new Date(); + await this.employeePosMasterRepository.save(posMaster); + return new HttpSuccess(); + } + /** * API รายละเอียดอัตรากำลัง * @@ -1369,6 +1398,8 @@ export class EmployeePositionController extends Controller { profilePoslevel: level == null || type == null ? null : `${type.posTypeShortName} ${level.posLevelName}`, authRoleId: posMaster.authRoleId, + isCondition: posMaster.isCondition, + conditionReason: posMaster.conditionReason, authRoleName: authRoleName == null || authRoleName.roleName == null ? null : authRoleName.roleName, positions: positions.map((position) => ({ diff --git a/src/entities/EmployeePosMaster.ts b/src/entities/EmployeePosMaster.ts index c1f0e143..d76b3e01 100644 --- a/src/entities/EmployeePosMaster.ts +++ b/src/entities/EmployeePosMaster.ts @@ -193,6 +193,20 @@ export class EmployeePosMaster extends EntityBase { }) authRoleId: string; + @Column({ + comment: "ตำแหน่งติดเงื่อนไข", + default: false, + }) + isCondition: boolean; + + @Column({ + nullable: true, + comment: "หมายเหตุตำแหน่งติดเงื่อนไข", + type: "text", + default: null, + }) + conditionReason: string; + @ManyToOne(() => AuthRole, (authRole) => authRole.posMasterEmps) @JoinColumn({ name: "authRoleId" }) authRole: AuthRole; diff --git a/src/migration/1771832659308-update_empPosMaster_add_fields_isCondition_.ts b/src/migration/1771832659308-update_empPosMaster_add_fields_isCondition_.ts new file mode 100644 index 00000000..98246b62 --- /dev/null +++ b/src/migration/1771832659308-update_empPosMaster_add_fields_isCondition_.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateEmpPosMasterAddFieldsIsCondition_1771832659308 implements MigrationInterface { + name = 'UpdateEmpPosMasterAddFieldsIsCondition_1771832659308' + + public async up(queryRunner: QueryRunner): Promise { + + await queryRunner.query(`ALTER TABLE \`employeePosMaster\` ADD \`isCondition\` tinyint NOT NULL COMMENT 'ตำแหน่งติดเงื่อนไข' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`employeePosMaster\` ADD \`conditionReason\` text NULL COMMENT 'หมายเหตุตำแหน่งติดเงื่อนไข'`); + + } + + public async down(queryRunner: QueryRunner): Promise { + + await queryRunner.query(`ALTER TABLE \`employeePosMaster\` DROP COLUMN \`conditionReason\``); + await queryRunner.query(`ALTER TABLE \`employeePosMaster\` DROP COLUMN \`isCondition\``); + + } + +} From d85d24527369d2f2a8934c35df1559c7e4512ef8 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 23 Feb 2026 16:37:50 +0700 Subject: [PATCH 108/178] #2221 --- src/services/PositionService.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 64fb58a4..651d374c 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -121,7 +121,7 @@ export async function CreatePosMasterHistoryEmployee( "positions", "positions.posLevel", "positions.posType", - "positions.posExecutive", + // "positions.posExecutive", "orgRoot", "orgChild1", "orgChild2", @@ -130,7 +130,6 @@ export async function CreatePosMasterHistoryEmployee( "current_holder", ], }); - if (!pm) return false; if (!pm.ancestorDNA) return false; const _null: any = null; @@ -190,7 +189,7 @@ export async function CreatePosMasterHistoryEmployeeTemp( "positions", "positions.posLevel", "positions.posType", - "positions.posExecutive", + // "positions.posExecutive", "orgRoot", "orgChild1", "orgChild2", From cdbdfce7af44a46a2454d3a8422dd2bee04bdc82 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 24 Feb 2026 13:28:57 +0700 Subject: [PATCH 109/178] Migrate update employeePosLevel.posLevelName Change Int to Number --- .../EmployeeTempPositionController.ts | 16 ++++++------ src/controllers/ImportDataController.ts | 18 ++++++++----- src/entities/EmployeePosLevel.ts | 10 ++++--- ...evel_change_datatype_field_posLevelName.ts | 26 +++++++++++++++++++ 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 src/migration/1771910056470-update_EmployeePosLevel_change_datatype_field_posLevelName.ts diff --git a/src/controllers/EmployeeTempPositionController.ts b/src/controllers/EmployeeTempPositionController.ts index bbb06cea..eca7ce6b 100644 --- a/src/controllers/EmployeeTempPositionController.ts +++ b/src/controllers/EmployeeTempPositionController.ts @@ -251,7 +251,7 @@ export class EmployeeTempPositionController extends Controller { switch (type) { case "positionName": findData = await this.employeePosDictRepository.find({ - where: { posDictName: Like(`%${keyword}%`), posLevel: { posLevelName: 1 } }, + where: { posDictName: Like(`%${keyword}%`), posLevel: { posLevelName: "1" } }, relations: ["posType", "posLevel"], order: { posDictName: "ASC", @@ -274,7 +274,7 @@ export class EmployeeTempPositionController extends Controller { select: ["id"], }); findData = await this.employeePosDictRepository.find({ - where: { posTypeId: In(findEmpTypes.map((x) => x.id)), posLevel: { posLevelName: 1 } }, + where: { posTypeId: In(findEmpTypes.map((x) => x.id)), posLevel: { posLevelName: "1" } }, relations: ["posType", "posLevel"], order: { posDictName: "ASC", @@ -292,19 +292,19 @@ export class EmployeeTempPositionController extends Controller { break; case "positionLevel": - if (!isNaN(Number(keyword))) { + if (!keyword) { let findEmpLevels; - if (Number(keyword) === 0) { + if (keyword === "0") { findEmpLevels = await this.employeePosLevelRepository.find(); } else { findEmpLevels = await this.employeePosLevelRepository.find({ - where: { posLevelName: Number(keyword) }, + where: { posLevelName: keyword }, }); } findData = await this.employeePosDictRepository.find({ where: { posLevelId: In(findEmpLevels.map((x) => x.id)), - posLevel: { posLevelName: 1 }, + posLevel: { posLevelName: "1" }, }, relations: ["posType", "posLevel"], order: { @@ -323,7 +323,7 @@ export class EmployeeTempPositionController extends Controller { } else { //กรณีเลือกค้นหาจาก"ระดับชั้นงาน" แต่กรอกไม่ใช่ number ให้ปล่อยมาหมดเลย findData = await this.employeePosDictRepository.find({ - where: { posLevel: { posLevelName: 1 } }, + where: { posLevel: { posLevelName: "1" } }, relations: ["posType", "posLevel"], order: { posDictName: "ASC", @@ -343,7 +343,7 @@ export class EmployeeTempPositionController extends Controller { default: findData = await this.employeePosDictRepository.find({ - where: { posLevel: { posLevelName: 1 } }, + where: { posLevel: { posLevelName: "1" } }, relations: ["posType", "posLevel"], order: { posDictName: "ASC", diff --git a/src/controllers/ImportDataController.ts b/src/controllers/ImportDataController.ts index 1c71f9b0..5b8ca808 100644 --- a/src/controllers/ImportDataController.ts +++ b/src/controllers/ImportDataController.ts @@ -395,12 +395,14 @@ export class ImportDataController extends Controller { } var positionType = ""; - var positionLevel = 0; + // var positionLevel = 0; + var positionLevel = "0"; const workLevel = item.WORK_LEVEL; const part1 = workLevel.split("/")[0]; // "ส 2" const value2 = part1.split(" ")[1]; // "2" if (value2) { - positionLevel = parseInt(value2); + // positionLevel = parseInt(value2); + positionLevel = value2; } if (item.CATEGORY_SAL_CODE == "11") { positionType = "บริการพื้นฐาน"; @@ -530,12 +532,14 @@ export class ImportDataController extends Controller { } var positionType = ""; - var positionLevel = 0; + // var positionLevel = 0; + var positionLevel = "0"; const value2 = item.POSITION_LEVEL; // const part1 = workLevel.split("/")[0]; // "ส 2" // const value2 = part1.split(" ")[1]; // "2" if (value2) { - positionLevel = parseInt(value2); + // positionLevel = parseInt(value2); + positionLevel = value2; } if (item.CATEGORY_SAL_CODE == "11") { positionType = "บริการพื้นฐาน"; @@ -4340,12 +4344,14 @@ export class ImportDataController extends Controller { let position = new EmployeePosition(); var positionType = ""; - var positionLevel = 0; + // var positionLevel = 0; + var positionLevel = "0"; const workLevel = item.WORK_LEVEL; const part1 = workLevel.split("/")[0]; // "ส 2" const value2 = part1.split(" ")[1]; // "2" if (value2) { - positionLevel = parseInt(value2); + // positionLevel = parseInt(value2); + positionLevel = value2; } if (item.CATEGORY_SAL_CODE == "11") { positionType = "บริการพื้นฐาน"; diff --git a/src/entities/EmployeePosLevel.ts b/src/entities/EmployeePosLevel.ts index 02759073..a9f67125 100644 --- a/src/entities/EmployeePosLevel.ts +++ b/src/entities/EmployeePosLevel.ts @@ -14,9 +14,13 @@ enum EmployeePosLevelAuthoritys { export class EmployeePosLevel extends EntityBase { @Column({ comment: "ชื่อระดับชั้นงาน", - type: "int", + // type: "int", + nullable: true, + length: 255, + default: null, }) - posLevelName: number; + // posLevelName: number; + posLevelName: string; @Column({ comment: "ระดับของระดับชั้นงาน", @@ -59,7 +63,7 @@ export class EmployeePosLevel extends EntityBase { export class CreateEmployeePosLevel { @Column() - posLevelName: number; + posLevelName: string; @Column() posLevelRank: number; diff --git a/src/migration/1771910056470-update_EmployeePosLevel_change_datatype_field_posLevelName.ts b/src/migration/1771910056470-update_EmployeePosLevel_change_datatype_field_posLevelName.ts new file mode 100644 index 00000000..740454f5 --- /dev/null +++ b/src/migration/1771910056470-update_EmployeePosLevel_change_datatype_field_posLevelName.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateEmployeePosLevelChangeDatatypeFieldPosLevelName1771910056470 implements MigrationInterface { + name = 'UpdateEmployeePosLevelChangeDatatypeFieldPosLevelName1771910056470' + + public async up(queryRunner: QueryRunner): Promise { + // await queryRunner.query(`ALTER TABLE \`employeePosLevel\` DROP COLUMN \`posLevelName\``); + // await queryRunner.query(`ALTER TABLE \`employeePosLevel\` ADD \`posLevelName\` varchar(255) NULL COMMENT 'ชื่อระดับชั้นงาน'`); + await queryRunner.query(` + ALTER TABLE \`employeePosLevel\` + MODIFY \`posLevelName\` varchar(255) NULL + COMMENT 'ชื่อระดับชั้นงาน' + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // await queryRunner.query(`ALTER TABLE \`employeePosLevel\` DROP COLUMN \`posLevelName\``); + // await queryRunner.query(`ALTER TABLE \`employeePosLevel\` ADD \`posLevelName\` int NOT NULL COMMENT 'ชื่อระดับชั้นงาน'`); + await queryRunner.query(` + ALTER TABLE \`employeePosLevel\` + MODIFY \`posLevelName\` int NOT NULL + COMMENT 'ชื่อระดับชั้นงาน' + `); + } + +} From 61c92088b15860526eb959672a57fb9917a2b096 Mon Sep 17 00:00:00 2001 From: Adisak Date: Tue, 24 Feb 2026 14:57:15 +0700 Subject: [PATCH 110/178] fix --- src/controllers/EmployeePositionController.ts | 3 ++- src/controllers/EmployeeTempPositionController.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/controllers/EmployeePositionController.ts b/src/controllers/EmployeePositionController.ts index 9baec79b..6fe825d1 100644 --- a/src/controllers/EmployeePositionController.ts +++ b/src/controllers/EmployeePositionController.ts @@ -1184,7 +1184,7 @@ export class EmployeePositionController extends Controller { ? _data.child1[0] != null ? `posMaster.orgChild1Id IN (:...child1)` // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `posMaster.orgChild1Id is null` + : `posMaster.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -1373,6 +1373,7 @@ export class EmployeePositionController extends Controller { return { id: posMaster.id, + ancestorDNA: posMaster.ancestorDNA, current_holderId: posMaster.current_holderId, orgRootId: posMaster.orgRootId, orgChild1Id: posMaster.orgChild1Id, diff --git a/src/controllers/EmployeeTempPositionController.ts b/src/controllers/EmployeeTempPositionController.ts index bbb06cea..c5e58580 100644 --- a/src/controllers/EmployeeTempPositionController.ts +++ b/src/controllers/EmployeeTempPositionController.ts @@ -902,7 +902,7 @@ export class EmployeeTempPositionController extends Controller { ? _data.child1[0] != null ? `posMaster.orgChild1Id IN (:...child1)` // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : `posMaster.orgChild1Id is null` + : `posMaster.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -1091,6 +1091,7 @@ export class EmployeeTempPositionController extends Controller { return { id: posMaster.id, + ancestorDNA: posMaster.ancestorDNA, current_holderId: posMaster.current_holderId, orgRootId: posMaster.orgRootId, orgChild1Id: posMaster.orgChild1Id, From 4018b9f7ceb3d1447b69bb848b1e614ff615b8c1 Mon Sep 17 00:00:00 2001 From: Adisak Date: Tue, 24 Feb 2026 17:12:23 +0700 Subject: [PATCH 111/178] fix:save dna --- src/controllers/EmployeePositionController.ts | 5 +++++ src/controllers/EmployeeTempPositionController.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/controllers/EmployeePositionController.ts b/src/controllers/EmployeePositionController.ts index 6fe825d1..39f239c4 100644 --- a/src/controllers/EmployeePositionController.ts +++ b/src/controllers/EmployeePositionController.ts @@ -679,6 +679,11 @@ export class EmployeePositionController extends Controller { posMaster.lastUpdateFullName = request.user.name; posMaster.lastUpdatedAt = new Date(); await this.employeePosMasterRepository.save(posMaster, { data: request }); + + const saved = await this.employeePosMasterRepository.save(posMaster, { data: request }); + saved.ancestorDNA = saved.id; + await this.employeePosMasterRepository.save(saved, { data: request }); + setLogDataDiff(request, { before, after: posMaster }); await Promise.all( requestBody.positions.map(async (x: any) => { diff --git a/src/controllers/EmployeeTempPositionController.ts b/src/controllers/EmployeeTempPositionController.ts index ad40ec22..d7ffdf62 100644 --- a/src/controllers/EmployeeTempPositionController.ts +++ b/src/controllers/EmployeeTempPositionController.ts @@ -546,6 +546,11 @@ export class EmployeeTempPositionController extends Controller { posMaster.lastUpdateFullName = request.user.name; posMaster.lastUpdatedAt = new Date(); await this.employeeTempPosMasterRepository.save(posMaster, { data: request }); + + const saved = await this.employeeTempPosMasterRepository.save(posMaster, { data: request }); + saved.ancestorDNA = saved.id; + await this.employeeTempPosMasterRepository.save(saved, { data: request }); + setLogDataDiff(request, { before, after: posMaster }); await Promise.all( requestBody.positions.map(async (x: any) => { From 34fb5321b13f2e64a9ca2ebbe59cd054c07025c2 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 24 Feb 2026 17:17:12 +0700 Subject: [PATCH 112/178] =?UTF-8?q?=E0=B9=80=E0=B8=A5=E0=B8=B7=E0=B8=AD?= =?UTF-8?q?=E0=B8=81=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88?= =?UTF-8?q?=E0=B8=87=E0=B9=80=E0=B8=9E=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B9=81?= =?UTF-8?q?=E0=B8=95=E0=B9=88=E0=B8=87=E0=B8=95=E0=B8=B1=E0=B9=89=E0=B8=87?= =?UTF-8?q?=E0=B8=A5=E0=B8=B9=E0=B8=81=E0=B8=88=E0=B9=89=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=8A=E0=B8=B1=E0=B9=88=E0=B8=A7=E0=B8=84=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=A7=20=E0=B9=81=E0=B8=AA=E0=B8=94=E0=B8=87=E0=B8=95?= =?UTF-8?q?=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=87=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B9=80=E0=B8=87?= =?UTF-8?q?=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/PositionController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index a71e8bbf..f35c3632 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -4723,6 +4723,8 @@ export class PositionController extends Controller { posMaster.next_holder == null ? null : `${posMaster.next_holder.prefix}${posMaster.next_holder.firstName} ${posMaster.next_holder.lastName}`, + isCondition: posMaster.isCondition, + conditionReason: posMaster.conditionReason, positions: posMaster.positions.map((position) => ({ id: position.id, positionName: position.positionName, From d7b45c322da34ce7d4f74e7badcda1babffe8373 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 24 Feb 2026 17:17:12 +0700 Subject: [PATCH 113/178] =?UTF-8?q?=E0=B9=80=E0=B8=A5=E0=B8=B7=E0=B8=AD?= =?UTF-8?q?=E0=B8=81=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88?= =?UTF-8?q?=E0=B8=87=E0=B9=80=E0=B8=9E=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B9=81?= =?UTF-8?q?=E0=B8=95=E0=B9=88=E0=B8=87=E0=B8=95=E0=B8=B1=E0=B9=89=E0=B8=87?= =?UTF-8?q?=E0=B8=A5=E0=B8=B9=E0=B8=81=E0=B8=88=E0=B9=89=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=8A=E0=B8=B1=E0=B9=88=E0=B8=A7=E0=B8=84=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=A7=20=E0=B9=81=E0=B8=AA=E0=B8=94=E0=B8=87=E0=B8=95?= =?UTF-8?q?=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=87=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B9=80=E0=B8=87?= =?UTF-8?q?=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/PositionController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index a71e8bbf..f35c3632 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -4723,6 +4723,8 @@ export class PositionController extends Controller { posMaster.next_holder == null ? null : `${posMaster.next_holder.prefix}${posMaster.next_holder.firstName} ${posMaster.next_holder.lastName}`, + isCondition: posMaster.isCondition, + conditionReason: posMaster.conditionReason, positions: posMaster.positions.map((position) => ({ id: position.id, positionName: position.positionName, From ea25979374c43a2f5114d1c6b8b102f3b366328d Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 25 Feb 2026 10:46:06 +0700 Subject: [PATCH 114/178] Fix bug Order posExecutiveNameOrg --- src/controllers/WorkflowController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index 01b53dd3..d2438547 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -1030,7 +1030,7 @@ export class WorkflowController extends Controller { if (body.sortBy) { queryBuilder = queryBuilder.orderBy( - `entity.${body.sortBy}`, + `entity.${body.sortBy === "posExecutiveNameOrg" ? "posExecutiveName" : body.sortBy}`, body.descending ? "DESC" : "ASC", ); } From 9946b7b7c302b839cb5bd965b12251c6a2a74711 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 26 Feb 2026 11:09:08 +0700 Subject: [PATCH 115/178] =?UTF-8?q?=E0=B9=80=E0=B8=A3=E0=B8=B5=E0=B8=A2?= =?UTF-8?q?=E0=B8=87=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=8A=E0=B8=B7=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=95=E0=B8=B2=E0=B8=A1=E0=B9=80=E0=B8=A5=E0=B8=82?= =?UTF-8?q?=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB?= =?UTF-8?q?=E0=B8=99=E0=B9=88=E0=B8=87=20#2325?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/OrganizationDotnetController.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 90633992..04d29f06 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -8607,7 +8607,10 @@ export class OrganizationDotnetController extends Controller { }), ); - return new HttpSuccess(profile_); + return new HttpSuccess( + (profile_ ?? []).sort((a, b) => + a.posNo.localeCompare(b.posNo, undefined, { numeric: true })) + ); } /** From 79dbba2c89cc6f422d82b75a4d11539cc2da22f9 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 26 Feb 2026 17:53:59 +0700 Subject: [PATCH 116/178] Fix #2343 --- src/controllers/CommandController.ts | 34 ++++++++++++---------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index d9428e27..aa2ab3d1 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -6245,17 +6245,17 @@ export class CommandController extends Controller { .orgRootShortName ?? ""; } } + const before = null; + const meta = { + createdUserId: req.user.sub, + createdFullName: req.user.name, + lastUpdateUserId: req.user.sub, + lastUpdateFullName: req.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; await Promise.all( body.data.map(async (item) => { - const before = null; - const meta = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; const _null: any = null; if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null; if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null; @@ -6306,13 +6306,6 @@ export class CommandController extends Controller { if (checkUser.length == 0) { let password = item.bodyProfile.citizenId; if (item.bodyProfile.birthDate != null) { - // const gregorianYear = item.bodyProfile.birthDate.getFullYear() + 543; - - // const formattedDate = - // item.bodyProfile.birthDate.toISOString().slice(8, 10) + - // item.bodyProfile.birthDate.toISOString().slice(5, 7) + - // gregorianYear; - // password = formattedDate; const _date = new Date(item.bodyProfile.birthDate.toDateString()) .getDate() .toString() @@ -6336,7 +6329,8 @@ export class CommandController extends Controller { name: x.name, })), ); - } else { + } + else { userKeycloakId = checkUser[0].id; const rolesData = await getRoleMappings(userKeycloakId); if (rolesData) { @@ -6397,12 +6391,12 @@ export class CommandController extends Controller { } await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE"); if (profileEmployee.keycloak != null) { - const delUserKeycloak = await deleteUser(profileEmployee.keycloak); - if (delUserKeycloak) { + // const delUserKeycloak = await deleteUser(profileEmployee.keycloak); + // if (delUserKeycloak) { profileEmployee.keycloak = _null; profileEmployee.roleKeycloaks = []; profileEmployee.isActive = false; - } + // } } profileEmployee.isLeave = true; profileEmployee.leaveReason = "บรรจุข้าราชการ"; From 625885973e693ec8787da13691f2d1fa5d21a825 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 21:25:07 +0700 Subject: [PATCH 117/178] completed sync script update keycloak --- scripts/KEYCLOAK_SYNC_README.md | 149 +++++ scripts/assign-user-role.ts | 269 ++++++++ scripts/clear-orphaned-users.ts | 80 +++ scripts/ensure-users.ts | 91 +++ scripts/sync-all.ts | 93 +++ scripts/sync-users-to-keycloak.ts | 327 ---------- src/app.ts | 44 +- src/controllers/KeycloakSyncController.ts | 184 ++++-- src/controllers/ScriptProfileOrgController.ts | 326 ++++++++++ src/keycloak/index.ts | 169 ++++- src/middlewares/auth.ts | 1 + src/middlewares/user.ts | 1 + src/services/KeycloakAttributeService.ts | 599 ++++++++++++++++-- 13 files changed, 1867 insertions(+), 466 deletions(-) create mode 100644 scripts/KEYCLOAK_SYNC_README.md create mode 100644 scripts/assign-user-role.ts create mode 100644 scripts/clear-orphaned-users.ts create mode 100644 scripts/ensure-users.ts create mode 100644 scripts/sync-all.ts delete mode 100644 scripts/sync-users-to-keycloak.ts create mode 100644 src/controllers/ScriptProfileOrgController.ts diff --git a/scripts/KEYCLOAK_SYNC_README.md b/scripts/KEYCLOAK_SYNC_README.md new file mode 100644 index 00000000..63d6deb1 --- /dev/null +++ b/scripts/KEYCLOAK_SYNC_README.md @@ -0,0 +1,149 @@ +# Keycloak Sync Scripts + +This directory contains standalone scripts for managing Keycloak users from the CLI. These scripts are useful for maintenance, setup, and troubleshooting Keycloak synchronization. + +## Prerequisites + +- Node.js and TypeScript installed +- Database connection configured in `.env` +- Keycloak connection configured in `.env` +- Run with `ts-node` or compile first + +## Environment Variables + +Ensure these are set in your `.env` file: + +```bash +# Database +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=your_database +DB_USER=your_user +DB_PASSWORD=your_password + +# Keycloak +KC_URL=https://your-keycloak-url +KC_REALMS=your-realm +KC_SERVICE_ACCOUNT_CLIENT_ID=your-client-id +KC_SERVICE_ACCOUNT_SECRET=your-client-secret +``` + +## Scripts + +### 1. clear-orphaned-users.ts + +Deletes Keycloak users that don't exist in the database (Profile + ProfileEmployee tables). Useful for cleaning up invalid users. + +**Protected usernames** (never deleted): `super_admin`, `admin_issue` + +```bash +# Dry run (preview changes) +ts-node scripts/clear-orphaned-users.ts --dry-run + +# Live execution +ts-node scripts/clear-orphaned-users.ts +``` + +**Output:** +- Total users in Keycloak +- Total users in Database +- Orphaned users found +- Deleted/Skipped/Failed counts + +### 2. ensure-users.ts + +Checks and creates Keycloak users for all profiles. Includes role assignment (USER role) automatically. + +```bash +# Dry run (preview changes) +ts-node scripts/ensure-users.ts --dry-run + +# Live execution +ts-node scripts/ensure-users.ts + +# Test with limited profiles (for testing) +ts-node scripts/ensure-users.ts --dry-run --limit=10 +``` + +**Output:** +- Total profiles processed +- Created users (new Keycloak accounts) +- Verified users (already exists) +- Skipped profiles (no citizenId) +- Failed count + +### 3. sync-all.ts + +Syncs all attributes to Keycloak for users with existing Keycloak IDs. + +**Attributes synced:** +- `profileId` +- `orgRootDnaId` +- `orgChild1DnaId` +- `orgChild2DnaId` +- `orgChild3DnaId` +- `orgChild4DnaId` +- `empType` +- `prefix` + +```bash +# Dry run (preview changes) +ts-node scripts/sync-all.ts --dry-run + +# Live execution +ts-node scripts/sync-all.ts + +# Test with limited profiles +ts-node scripts/sync-all.ts --dry-run --limit=10 + +# Adjust concurrency (default: 5) +ts-node scripts/sync-all.ts --concurrency=10 +``` + +**Output:** +- Total profiles with Keycloak IDs +- Success/Failed counts +- Error details for failures + +## Recommended Execution Order + +For initial setup or full resynchronization: + +1. **clear-orphaned-users** - Clean up invalid users first + ```bash + npx ts-node scripts/clear-orphaned-users.ts + ``` + +2. **ensure-users** - Create missing users and assign roles + ```bash + npx ts-node scripts/ensure-users.ts + ``` + +3. **sync-all** - Sync all attributes to Keycloak + ```bash + npx ts-node scripts/sync-all.ts + ``` + +## Tips + +- Always run with `--dry-run` first to preview changes +- Use `--limit=N` for testing before running on full dataset +- Scripts process both Profile (ข้าราชการ) and ProfileEmployee (ลูกจ้าง) tables +- Only active profiles (`isLeave: false`) are processed by ensure-users +- The `USER` role is automatically assigned to new/verified users + +## Troubleshooting + +**Database connection error:** +- Check `.env` database variables +- Ensure database server is running + +**Keycloak connection error:** +- Check `KC_URL`, `KC_REALMS` in `.env` +- Verify service account credentials +- Check network connectivity to Keycloak + +**USER role not found:** +- Log in to Keycloak admin console +- Create a `USER` role in your realm +- Ensure service account has `manage-users` and `view-users` permissions diff --git a/scripts/assign-user-role.ts b/scripts/assign-user-role.ts new file mode 100644 index 00000000..3a1ca878 --- /dev/null +++ b/scripts/assign-user-role.ts @@ -0,0 +1,269 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { Profile } from "../src/entities/Profile"; +import { ProfileEmployee } from "../src/entities/ProfileEmployee"; +import * as keycloak from "../src/keycloak/index"; + +const USER_ROLE_NAME = "USER"; + +interface AssignOptions { + dryRun: boolean; + targetUsernames?: string[]; +} + +interface UserWithKeycloak { + keycloakId: string; + citizenId: string; + source: "Profile" | "ProfileEmployee"; +} + +interface AssignResult { + total: number; + assigned: number; + skipped: number; + failed: number; + errors: Array<{ + userId: string; + username: string; + error: string; + }>; +} + +/** + * Get all users from database who have Keycloak IDs set + */ +async function getUsersWithKeycloak(): Promise { + const users: UserWithKeycloak[] = []; + const profileRepo = AppDataSource.getRepository(Profile); + const profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); + + // Get from Profile table + const profiles = await profileRepo + .createQueryBuilder("profile") + .where("profile.keycloak IS NOT NULL") + .andWhere("profile.keycloak != ''") + .getMany(); + + for (const profile of profiles) { + users.push({ + keycloakId: profile.keycloak, + citizenId: profile.citizenId || profile.id, + source: "Profile", + }); + } + + // Get from ProfileEmployee table + const employees = await profileEmployeeRepo + .createQueryBuilder("profileEmployee") + .where("profileEmployee.keycloak IS NOT NULL") + .andWhere("profileEmployee.keycloak != ''") + .getMany(); + + for (const employee of employees) { + // Avoid duplicates - check if keycloak ID already exists + if (!users.some((u) => u.keycloakId === employee.keycloak)) { + users.push({ + keycloakId: employee.keycloak, + citizenId: employee.citizenId || employee.id, + source: "ProfileEmployee", + }); + } + } + + return users; +} + +/** + * Assign USER role to users who don't have it + */ +async function assignUserRoleToUsers( + users: UserWithKeycloak[], + userRoleId: string, + options: AssignOptions, +): Promise { + const result: AssignResult = { + total: users.length, + assigned: 0, + skipped: 0, + failed: 0, + errors: [], + }; + + console.log(`Processing ${result.total} users...`); + + for (let i = 0; i < users.length; i++) { + const user = users[i]; + const index = i + 1; + + try { + // Get user's current roles + const userRoles = await keycloak.getUserRoles(user.keycloakId); + + if (!userRoles || typeof userRoles === "boolean") { + console.log( + `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Failed to get roles`, + ); + result.failed++; + result.errors.push({ + userId: user.keycloakId, + username: user.citizenId, + error: "Failed to get user roles", + }); + continue; + } + + // Handle both array and single object return types + // getUserRoles can return an array or a single object + const rolesArray = Array.isArray(userRoles) ? userRoles : [userRoles]; + + // Check if user already has USER role + const hasUserRole = rolesArray.some((role: { id: string; name: string }) => + role.name === USER_ROLE_NAME, + ); + + if (hasUserRole) { + console.log( + `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Already has USER role`, + ); + result.skipped++; + continue; + } + + // Assign USER role + if (options.dryRun) { + console.log( + `[${index}/${result.total}] [DRY-RUN] Would assign USER role: ${user.citizenId} (source: ${user.source})`, + ); + result.assigned++; + } else { + const assignResult = await keycloak.addUserRoles(user.keycloakId, [ + { id: userRoleId, name: USER_ROLE_NAME }, + ]); + + if (assignResult) { + console.log( + `[${index}/${result.total}] Assigned USER role: ${user.citizenId} (source: ${user.source})`, + ); + result.assigned++; + } else { + console.log( + `[${index}/${result.total}] Failed: ${user.citizenId} (source: ${user.source}) - Could not assign role`, + ); + result.failed++; + result.errors.push({ + userId: user.keycloakId, + username: user.citizenId, + error: "Failed to assign USER role", + }); + } + } + + // Small delay to avoid rate limiting + if (index % 50 === 0) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.log( + `[${index}/${result.total}] Error: ${user.citizenId} (source: ${user.source}) - ${errorMessage}`, + ); + result.failed++; + result.errors.push({ + userId: user.keycloakId, + username: user.citizenId, + error: errorMessage, + }); + } + } + + return result; +} + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + + console.log("=".repeat(60)); + console.log("Keycloak USER Role Assignment Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + // Get USER role from Keycloak + console.log(""); + const userRole = await keycloak.getRoles(USER_ROLE_NAME); + + // Check if USER role exists and is valid (has id property) + if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { + console.error(`ERROR: ${USER_ROLE_NAME} role not found in Keycloak`); + await AppDataSource.destroy(); + process.exit(1); + } + + // Type assertion via unknown to bypass union type issues + const role = userRole as unknown as { id: string; name: string; description?: string }; + const roleId = role.id; + console.log(`${USER_ROLE_NAME} role found: ${roleId}`); + console.log(""); + + // Get users from database + console.log("Fetching users from database..."); + let users = await getUsersWithKeycloak(); + + if (users.length === 0) { + console.log("No users with Keycloak IDs found in database"); + await AppDataSource.destroy(); + process.exit(0); + } + + console.log(`Found ${users.length} users with Keycloak IDs`); + console.log(""); + + // Assign USER role + const result = await assignUserRoleToUsers(users, roleId, { dryRun }); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total users: ${result.total}`); + console.log(` Assigned: ${result.assigned}`); + console.log(` Skipped: ${result.skipped}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.errors.length > 0) { + console.log(""); + console.log("Errors:"); + result.errors.forEach((e) => { + console.log(` ${e.username}: ${e.error}`); + }); + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/scripts/clear-orphaned-users.ts b/scripts/clear-orphaned-users.ts new file mode 100644 index 00000000..67eee99d --- /dev/null +++ b/scripts/clear-orphaned-users.ts @@ -0,0 +1,80 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; +import * as keycloak from "../src/keycloak/index"; + +const PROTECTED_USERNAMES = ["super_admin", "admin_issue"]; + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const skipUsernames = [...PROTECTED_USERNAMES]; + + console.log("=".repeat(60)); + console.log("Clear Orphaned Keycloak Users Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + console.log(`Protected usernames: ${skipUsernames.join(", ")}`); + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log(""); + + // Run the orphaned user cleanup + const service = new KeycloakAttributeService(); + const result = await service.clearOrphanedKeycloakUsers(skipUsernames); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total users in Keycloak: ${result.totalInKeycloak}`); + console.log(` Total users in Database: ${result.totalInDatabase}`); + console.log(` Orphaned users: ${result.orphanedCount}`); + console.log(` Deleted: ${result.deleted}`); + console.log(` Skipped: ${result.skipped}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.details.length > 0) { + console.log(""); + console.log("Details:"); + for (const detail of result.details) { + const status = + detail.action === "deleted" + ? "[DELETED]" + : detail.action === "skipped" + ? "[SKIPPED]" + : "[ERROR]"; + console.log( + ` ${status} ${detail.username} (${detail.keycloakUserId})${detail.error ? ": " + detail.error : ""}`, + ); + } + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/scripts/ensure-users.ts b/scripts/ensure-users.ts new file mode 100644 index 00000000..14522d96 --- /dev/null +++ b/scripts/ensure-users.ts @@ -0,0 +1,91 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; +import * as keycloak from "../src/keycloak/index"; + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const limitArg = args.find((arg) => arg.startsWith("--limit=")); + const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; + + console.log("=".repeat(60)); + console.log("Ensure Keycloak Users Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + if (limit !== undefined) { + console.log(`Limit: ${limit} profiles per table (for testing)`); + } + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log(""); + + // Verify USER role exists + console.log("Verifying USER role in Keycloak..."); + const userRole = await keycloak.getRoles("USER"); + + if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { + console.error("ERROR: USER role not found in Keycloak"); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log("USER role found"); + console.log(""); + + // Run the ensure users operation + const service = new KeycloakAttributeService(); + console.log("Ensuring Keycloak users for all profiles..."); + console.log(""); + + const result = await service.batchEnsureKeycloakUsers(); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total profiles: ${result.total}`); + console.log(` Created: ${result.created}`); + console.log(` Verified: ${result.verified}`); + console.log(` Skipped: ${result.skipped}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.failed > 0) { + console.log(""); + console.log("Failed Details:"); + const failedDetails = result.details.filter((d) => d.action === "error" || !!d.error); + for (const detail of failedDetails) { + console.log( + ` [${detail.profileType}] ${detail.profileId}: ${detail.error || "Unknown error"}`, + ); + } + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/scripts/sync-all.ts b/scripts/sync-all.ts new file mode 100644 index 00000000..9090dd17 --- /dev/null +++ b/scripts/sync-all.ts @@ -0,0 +1,93 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; +import * as keycloak from "../src/keycloak/index"; + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const limitArg = args.find((arg) => arg.startsWith("--limit=")); + const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; + const concurrencyArg = args.find((arg) => arg.startsWith("--concurrency=")); + const concurrency = concurrencyArg ? parseInt(concurrencyArg.split("=")[1], 10) : undefined; + + console.log("=".repeat(60)); + console.log("Sync All Attributes to Keycloak Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + if (limit !== undefined) { + console.log(`Limit: ${limit} profiles per table (for testing)`); + } + if (concurrency !== undefined) { + console.log(`Concurrency: ${concurrency}`); + } + console.log(""); + + console.log("Attributes to sync:"); + console.log(" - profileId"); + console.log(" - orgRootDnaId"); + console.log(" - orgChild1DnaId"); + console.log(" - orgChild2DnaId"); + console.log(" - orgChild3DnaId"); + console.log(" - orgChild4DnaId"); + console.log(" - empType"); + console.log(" - prefix"); + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log(""); + + // Run the sync operation + const service = new KeycloakAttributeService(); + console.log("Syncing attributes for all profiles with Keycloak IDs..."); + console.log(""); + + const result = await service.batchSyncUsers({ limit, concurrency }); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total profiles: ${result.total}`); + console.log(` Success: ${result.success}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.failed > 0) { + console.log(""); + console.log("Failed Details:"); + for (const detail of result.details.filter( + (d) => d.status === "failed" || d.status === "error", + )) { + console.log( + ` ${detail.profileId} (${detail.keycloakUserId}): ${detail.error || "Sync failed"}`, + ); + } + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/scripts/sync-users-to-keycloak.ts b/scripts/sync-users-to-keycloak.ts deleted file mode 100644 index 93b4660f..00000000 --- a/scripts/sync-users-to-keycloak.ts +++ /dev/null @@ -1,327 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { Profile } from "../src/entities/Profile"; -import { RoleKeycloak } from "../src/entities/RoleKeycloak"; -import * as keycloak from "../src/keycloak/index"; - -// Default role for users without roles -const DEFAULT_ROLE = "USER"; - -interface SyncOptions { - dryRun: boolean; - delay: number; -} - -interface SyncResult { - total: number; - deleted: number; - created: number; - failed: number; - skipped: number; - errors: Array<{ - profileId: string; - citizenId: string; - error: string; - }>; -} - -/** - * Delete all Keycloak users (except super_admin) - */ -async function deleteAllKeycloakUsers(dryRun: boolean): Promise { - const users = await keycloak.getUserList("0", "-1"); - let count = 0; - let skipped = 0; - - if (!users || typeof users === "boolean") { - return 0; - } - - for (const user of users) { - // Skip super_admin user - if (user.username === "super_admin") { - console.log(`Skipped super_admin user (protected)`); - skipped++; - continue; - } - - if (!dryRun) { - await keycloak.deleteUser(user.id); - } - count++; - console.log(`[${count}/${users.length}] Deleted user: ${user.username}`); - } - - console.log(`Skipped ${skipped} protected user(s)`); - return count; -} - -/** - * Sync profiles to Keycloak - */ -async function syncProfiles(options: SyncOptions): Promise { - const result: SyncResult = { - total: 0, - deleted: 0, - created: 0, - failed: 0, - skipped: 0, - errors: [], - }; - - // Fetch all Keycloak roles first - const keycloakRoles = await keycloak.getRoles(); - if (!keycloakRoles || typeof keycloakRoles === "boolean") { - throw new Error("Failed to get Keycloak roles"); - } - const roleMap = new Map(keycloakRoles.map((r) => [r.name, r.id])); - - // Log all available Keycloak roles for debugging - console.log("Available Keycloak roles:", Array.from(roleMap.keys()).sort()); - console.log(""); - - // Get repositories - const profileRepo = AppDataSource.getRepository(Profile); - const roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); - - // Query all active profiles (not just those with keycloak set) - const profiles = await profileRepo - .createQueryBuilder("profile") - .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloak") - .andWhere("profile.isActive = :isActive", { isActive: true }) - .getMany(); - - result.total = profiles.length; - - for (const profile of profiles) { - const index = result.created + result.failed + result.skipped + 1; - - try { - // Validate required fields - if (!profile.citizenId) { - console.log(`[${index}/${result.total}] Skipped: Missing citizenId`); - result.skipped++; - continue; - } - - // Check if user already exists - const existingUser = await keycloak.getUserByUsername(profile.citizenId); - if (existingUser && existingUser.length > 0) { - const existingUserId = existingUser[0].id; - console.log( - `[${index}/${result.total}] User ${profile.citizenId} already exists in Keycloak (ID: ${existingUserId})`, - ); - - // Update profile.keycloak with existing Keycloak ID - if (!options.dryRun) { - await profileRepo.update(profile.id, { keycloak: existingUserId }); - console.log(` -> Updated profile.keycloak: ${existingUserId}`); - } - - result.skipped++; - continue; - } - - // Handle roles: assign USER role if no roles exist - let rolesToAssign = profile.roleKeycloaks || []; - let needsDefaultRole = false; - - if (rolesToAssign.length === 0) { - needsDefaultRole = true; - console.log( - `[${index}/${result.total}] No roles found for ${profile.citizenId}, will assign ${DEFAULT_ROLE}`, - ); - - // Check if USER role exists in Keycloak - if (!roleMap.has(DEFAULT_ROLE)) { - console.log( - `[${index}/${result.total}] ERROR: ${DEFAULT_ROLE} role not found in Keycloak`, - ); - result.failed++; - result.errors.push({ - profileId: profile.id, - citizenId: profile.citizenId, - error: `${DEFAULT_ROLE} role not found in Keycloak`, - }); - continue; - } - - // In live mode, create role in database - if (!options.dryRun) { - // Check if USER role record exists in database - const userRoleRecord = await roleKeycloakRepo.findOne({ - where: { name: DEFAULT_ROLE }, - }); - - // Assign role to profile in database - if (userRoleRecord) { - profile.roleKeycloaks = [userRoleRecord]; - await profileRepo.save(profile); - rolesToAssign = [userRoleRecord]; - } - } - } - - if (!options.dryRun) { - // Create user in Keycloak - const userId = await keycloak.createUser(profile.citizenId, "P@ssw0rd", { - firstName: profile.firstName || "", - lastName: profile.lastName || "", - // email: profile.email || undefined, - enabled: true, - }); - - if (typeof userId === "string") { - // Update profile.keycloak with new ID - await profileRepo.update(profile.id, { keycloak: userId }); - - // Assign roles to user in Keycloak - // Track which roles exist in Keycloak and which don't - const validRoles: { id: string; name: string }[] = []; - const missingRoles: string[] = []; - - for (const rk of rolesToAssign) { - const roleId = roleMap.get(rk.name); - if (roleId) { - validRoles.push({ id: roleId, name: rk.name }); - } else { - missingRoles.push(rk.name); - } - } - - // Warn about missing roles - if (missingRoles.length > 0) { - console.log(` [WARNING] Roles not found in Keycloak: ${missingRoles.join(", ")}`); - } - - if (validRoles.length > 0) { - const addRolesResult = await keycloak.addUserRoles(userId, validRoles); - if (addRolesResult === false) { - console.log(` [WARNING] Failed to assign roles to user ${profile.citizenId}`); - } - const roleNames = validRoles.map((r) => r.name).join(", "); - console.log( - `[${index}/${result.total}] Created: ${profile.citizenId} -> ${userId} [Roles: ${roleNames}]`, - ); - } else { - console.log( - `[${index}/${result.total}] Created: ${profile.citizenId} -> ${userId} [No roles assigned]`, - ); - } - - result.created++; - } else { - console.log(`[${index}/${result.total}] Failed: ${profile.citizenId}`, userId); - result.failed++; - result.errors.push({ - profileId: profile.id, - citizenId: profile.citizenId, - error: JSON.stringify(userId), - }); - } - } else { - // Dry-run mode - check which roles are valid - const validRoles: string[] = []; - const missingRoles: string[] = []; - - for (const rk of rolesToAssign) { - if (roleMap.has(rk.name)) { - validRoles.push(rk.name); - } else { - missingRoles.push(rk.name); - } - } - - if (needsDefaultRole && roleMap.has(DEFAULT_ROLE)) { - validRoles.push(DEFAULT_ROLE); - } else if (needsDefaultRole) { - missingRoles.push(DEFAULT_ROLE); - } - - const roleNames = validRoles.length > 0 ? validRoles.join(", ") : "None"; - console.log( - `[DRY-RUN ${index}/${result.total}] Would create: ${profile.citizenId} [Roles: ${roleNames}]`, - ); - - if (missingRoles.length > 0) { - console.log(` [WARNING] Roles not found in Keycloak: ${missingRoles.join(", ")}`); - } - - result.created++; - } - - // Delay to avoid rate limiting - if (options.delay > 0) { - await new Promise((resolve) => setTimeout(resolve, options.delay)); - } - } catch (error) { - console.log(`[${index}/${result.total}] Error: ${profile.citizenId}`, error); - result.failed++; - result.errors.push({ - profileId: profile.id, - citizenId: profile.citizenId, - error: String(error), - }); - } - } - - return result; -} - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - - console.log("=".repeat(60)); - console.log("Keycloak User Sync Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - console.log(""); - - // Initialize database - await AppDataSource.initialize(); - console.log("Database connected"); - - // Validate Keycloak connection - await keycloak.getToken(); - console.log("Keycloak connected"); - console.log(""); - - // Step 1: Delete existing users - console.log("Step 1: Deleting existing Keycloak users..."); - const deletedCount = await deleteAllKeycloakUsers(dryRun); - console.log(`Deleted ${deletedCount} users\n`); - - // Step 2: Sync profiles - console.log("Step 2: Creating users from profiles..."); - const result = await syncProfiles({ dryRun, delay: 100 }); - console.log(""); - - // Summary - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total profiles: ${result.total}`); - console.log(` Deleted users: ${deletedCount}`); - console.log(` Created users: ${result.created}`); - console.log(` Failed: ${result.failed}`); - console.log(` Skipped: ${result.skipped}`); - console.log("=".repeat(60)); - - if (result.errors.length > 0) { - console.log("\nErrors:"); - result.errors.forEach((e) => { - console.log(` ${e.citizenId}: ${e.error}`); - }); - } - - await AppDataSource.destroy(); -} - -main().catch(console.error); - -// add this line to package.json scripts section: -// "sync-keycloak": "ts-node scripts/sync-users-to-keycloak-null-only.ts", -// "sync-keycloak:dry": "ts-node scripts/sync-users-to-keycloak-null-only.ts --dry-run" diff --git a/src/app.ts b/src/app.ts index 75d0bfea..06f76548 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,6 +15,7 @@ import { logMemoryStore } from "./utils/LogMemoryStore"; import { orgStructureCache } from "./utils/OrgStructureCache"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; +import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; @@ -52,19 +53,8 @@ async function main() { const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_PORT = +(process.env.APP_PORT || 3000); - const cronTime = "0 0 3 * * *"; // ตั้งเวลาทุกวันเวลา 03:00:00 - // const cronTime = "*/10 * * * * *"; - cron.schedule(cronTime, async () => { - try { - const orgController = new OrganizationController(); - await orgController.cronjobRevision(); - } catch (error) { - console.error("Error executing function from controller:", error); - } - }); - - const cronTime_command = "0 0 2 * * *"; - // const cronTime_command = "*/10 * * * * *"; + // Cron job for executing command - every day at 00:30:00 + const cronTime_command = "0 30 0 * * *"; cron.schedule(cronTime_command, async () => { try { const commandController = new CommandController(); @@ -74,7 +64,19 @@ async function main() { } }); - const cronTime_Oct = "0 0 1 10 *"; + // Cron job for updating org revision - every day at 01:00:00 + const cronTime = "0 0 1 * * *"; + cron.schedule(cronTime, async () => { + try { + const orgController = new OrganizationController(); + await orgController.cronjobRevision(); + } catch (error) { + console.error("Error executing function from controller:", error); + } + }); + + // Cron job for updating retirement status - every day at 02:00:00 on the 1st of October + const cronTime_Oct = "0 0 2 10 *"; cron.schedule(cronTime_Oct, async () => { try { const commandController = new CommandController(); @@ -84,7 +86,19 @@ async function main() { } }); - const cronTime_Tenure = "0 0 0 * * *"; + // Cron job for updating org DNA - every day at 03:00:00 + const cronTime_UpdateOrg = "0 0 3 * * *"; + cron.schedule(cronTime_UpdateOrg, async () => { + try { + const scriptProfileOrgController = new ScriptProfileOrgController(); + await scriptProfileOrgController.cronjobUpdateOrg({} as any); + } catch (error) { + console.error("Error executing cronjobUpdateOrg:", error); + } + }); + + // Cron job for updating tenure - every day at 04:00:00 + const cronTime_Tenure = "0 0 4 * * *"; cron.schedule(cronTime_Tenure, async () => { try { const profileSalaryController = new ProfileSalaryController(); diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts index 2a55983c..5f814238 100644 --- a/src/controllers/KeycloakSyncController.ts +++ b/src/controllers/KeycloakSyncController.ts @@ -1,12 +1,21 @@ -import { Controller, Post, Get, Route, Security, Tags, Path, Request, Response, Query } from "tsoa"; +import { + Controller, + Post, + Get, + Route, + Security, + Tags, + Path, + Request, + Response, + Query, + Body, +} from "tsoa"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; import HttpSuccess from "../interfaces/http-success"; import HttpStatus from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; import { RequestWithUser } from "../middlewares/user"; -import { AppDataSource } from "../database/data-source"; -import { Profile } from "../entities/Profile"; -import { ProfileEmployee } from "../entities/ProfileEmployee"; @Route("api/v1/org/keycloak-sync") @Tags("Keycloak Sync") @@ -17,8 +26,6 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; ) export class KeycloakSyncController extends Controller { private keycloakAttributeService = new KeycloakAttributeService(); - private profileRepo = AppDataSource.getRepository(Profile); - private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); /** * Sync attributes for the current logged-in user @@ -62,7 +69,7 @@ export class KeycloakSyncController extends Controller { * * @summary Get current profileId and rootDnaId from Keycloak */ - @Get("my-attributes") + // @Get("my-attributes") async getMyAttributes(@Request() request: RequestWithUser) { const keycloakUserId = request.user.sub; @@ -117,19 +124,80 @@ export class KeycloakSyncController extends Controller { } /** - * Batch sync all users (Admin only) + * Batch sync attributes for multiple profiles (Admin only) * - * @summary Batch sync all users to Keycloak (ADMIN) + * @summary Batch sync profileId and rootDnaId to Keycloak for multiple profiles (ADMIN) * - * @param {number} limit Maximum number of users to sync + * @param {request} request Request body containing profileIds array and profileType */ - @Post("sync-all") - async syncAll(@Query() limit: number = 100) { - if (limit > 500) { - throw new HttpError(HttpStatus.BAD_REQUEST, "limit ต้องไม่เกิน 500"); + // @Post("sync-profiles-batch") + async syncByProfileIds( + @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" }, + ) { + const { profileIds, profileType } = request; + + // Validate profileIds + if (!profileIds || profileIds.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า"); } - const result = await this.keycloakAttributeService.batchSyncUsers(limit); + // Validate profileType + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + const result = { + total: profileIds.length, + success: 0, + failed: 0, + details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>, + }; + + // Process each profileId + for (const profileId of profileIds) { + try { + const success = await this.keycloakAttributeService.syncOnOrganizationChange( + profileId, + profileType, + ); + + if (success) { + result.success++; + result.details.push({ profileId, status: "success" }); + } else { + result.failed++; + result.details.push({ + profileId, + status: "failed", + error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ profileId, status: "failed", error: error.message }); + } + } + + return new HttpSuccess({ + message: "Batch sync เสร็จสิ้น", + ...result, + }); + } + + /** + * Batch sync all users (Admin only) + * + * @summary Batch sync all users to Keycloak without limit (ADMIN) + * + * @description Syncs profileId and orgRootDnaId to Keycloak for all users + * that have a keycloak ID. Uses parallel processing for better performance. + */ + // @Post("sync-all") + async syncAll() { + const result = await this.keycloakAttributeService.batchSyncUsers(); return new HttpSuccess({ message: "Batch sync เสร็จสิ้น", @@ -141,58 +209,46 @@ export class KeycloakSyncController extends Controller { } /** - * ตรวจสอบสถานะ Keycloak Mapper + * Ensure Keycloak users exist for all profiles (Admin only) * - * @summary ตรวจสอบว่า profileId และ rootDnaId ออกมาใน token หรือไม่ + * @summary Create or verify Keycloak users for all profiles in Profile and ProfileEmployee tables (ADMIN) + * + * @description + * This endpoint will: + * - Create new Keycloak users for profiles without a keycloak ID + * - Create new Keycloak users for profiles where the stored keycloak ID doesn't exist in Keycloak + * - Verify existing Keycloak users + * - Skip profiles without a citizenId */ - @Get("check-mapper") - async checkMapperStatus(@Request() request: RequestWithUser) { - const keycloakUserId = request.user.sub; - - if (!keycloakUserId) { - throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); - } - - // 1. ตรวจสอบ attributes ใน Keycloak - const kcAttrs = - await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); - - // 2. ตรวจสอบ attributes ใน Database - const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); - - // 3. ตรวจสอบ token payload ปัจจุบัน - const tokenPayload = request.user; - + // @Post("ensure-users") + async ensureAllUsers() { + const result = await this.keycloakAttributeService.batchEnsureKeycloakUsers(); return new HttpSuccess({ - keycloakAttributes: kcAttrs, - databaseAttributes: dbAttrs, - tokenHasProfileId: !!tokenPayload.profileId, - tokenHasOrgRootDnaId: !!tokenPayload.orgRootDnaId, - tokenScopes: tokenPayload.scope?.split(" ") || [], - diagnosis: { - kcHasProfileId: !!kcAttrs?.profileId, - kcHasOrgRootDnaId: !!kcAttrs?.orgRootDnaId, - kcHasOrgChild1DnaId: !!kcAttrs?.orgChild1DnaId, - kcHasOrgChild2DnaId: !!kcAttrs?.orgChild2DnaId, - kcHasOrgChild3DnaId: !!kcAttrs?.orgChild3DnaId, - kcHasOrgChild4DnaId: !!kcAttrs?.orgChild4DnaId, - kcHasEmpType: !!kcAttrs?.empType, - dbHasProfileId: !!dbAttrs?.profileId, - dbHasOrgRootDnaId: !!dbAttrs?.orgRootDnaId, - dbHasOrgChild1DnaId: !!dbAttrs?.orgChild1DnaId, - dbHasOrgChild2DnaId: !!dbAttrs?.orgChild2DnaId, - dbHasOrgChild3DnaId: !!dbAttrs?.orgChild3DnaId, - dbHasOrgChild4DnaId: !!dbAttrs?.orgChild4DnaId, - dbHasEmpType: !!dbAttrs?.empType, - issue: - !tokenPayload.profileId && kcAttrs?.profileId - ? "Attribute มีใน Keycloak แต่ไม่ออกมาใน token - แก้ไข Mapper หรือ Client Scope" - : !kcAttrs?.profileId && dbAttrs?.profileId - ? "Attribute มีใน Database แต่ไม่มีใน Keycloak - ต้อง sync ซ้ำ" - : !dbAttrs?.profileId - ? "ไม่พบ profile ใน database - ตรวจสอบ keycloak field" - : "ทุกอย่างปกติ", - }, + message: "Batch ensure Keycloak users เสร็จสิ้น", + ...result, + }); + } + + /** + * Clear orphaned Keycloak users (Admin only) + * + * @summary Delete Keycloak users that are not in the database (ADMIN) + * + * @description + * This endpoint will: + * - Find users in Keycloak that are not referenced in Profile or ProfileEmployee tables + * - Delete those orphaned users from Keycloak + * - Skip protected users (super_admin, admin_issue) + * + * @param {request} request Request body containing skipUsernames array + */ + // @Post("clear-orphaned-users") + async clearOrphanedUsers(@Body() request?: { skipUsernames?: string[] }) { + const skipUsernames = request?.skipUsernames || ["super_admin", "admin_issue"]; + const result = await this.keycloakAttributeService.clearOrphanedKeycloakUsers(skipUsernames); + return new HttpSuccess({ + message: "Clear orphaned Keycloak users เสร็จสิ้น", + ...result, }); } } diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts new file mode 100644 index 00000000..c8975e43 --- /dev/null +++ b/src/controllers/ScriptProfileOrgController.ts @@ -0,0 +1,326 @@ +import { Controller, Post, Route, Security, Tags, Request } from "tsoa"; +import { AppDataSource } from "../database/data-source"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; +import { MoreThanOrEqual } from "typeorm"; +import { PosMaster } from "./../entities/PosMaster"; +import axios from "axios"; +import { KeycloakSyncController } from "./KeycloakSyncController"; +import { EmployeePosMaster } from "./../entities/EmployeePosMaster"; + +interface OrgUpdatePayload { + profileId: string; + rootDnaId: string | null; + child1DnaId: string | null; + child2DnaId: string | null; + child3DnaId: string | null; + child4DnaId: string | null; + profileType: "PROFILE" | "PROFILE_EMPLOYEE"; +} + +@Route("api/v1/org/script-profile-org") +@Tags("Keycloak Sync") +@Security("bearerAuth") +export class ScriptProfileOrgController extends Controller { + private posMasterRepo = AppDataSource.getRepository(PosMaster); + private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); + + // Idempotency flag to prevent concurrent runs + private isRunning = false; + + // Configurable values + private readonly BATCH_SIZE = parseInt(process.env.CRONJOB_BATCH_SIZE || "100", 10); + private readonly UPDATE_WINDOW_HOURS = parseInt( + process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24", + 10, + ); + + @Post("update-org") + public async cronjobUpdateOrg(@Request() request: RequestWithUser) { + // Idempotency check - prevent concurrent runs + if (this.isRunning) { + console.log("cronjobUpdateOrg: Job already running, skipping this execution"); + return new HttpSuccess({ + message: "Job already running", + skipped: true, + }); + } + + this.isRunning = true; + const startTime = Date.now(); + + try { + const windowStart = new Date(Date.now() - this.UPDATE_WINDOW_HOURS * 60 * 60 * 1000); + + console.log("cronjobUpdateOrg: Starting job", { + windowHours: this.UPDATE_WINDOW_HOURS, + windowStart: windowStart.toISOString(), + batchSize: this.BATCH_SIZE, + }); + + // Query with optimized select - only fetch required fields + const [posMasters, posMasterEmployee] = await Promise.all([ + this.posMasterRepo.find({ + where: { + lastUpdatedAt: MoreThanOrEqual(windowStart), + orgRevision: { + orgRevisionIsCurrent: true, + }, + }, + relations: [ + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], + select: { + id: true, + current_holderId: true, + lastUpdatedAt: true, + orgRevision: { id: true }, + orgRoot: { ancestorDNA: true }, + orgChild1: { ancestorDNA: true }, + orgChild2: { ancestorDNA: true }, + orgChild3: { ancestorDNA: true }, + orgChild4: { ancestorDNA: true }, + current_holder: { id: true }, + }, + }), + this.employeePosMasterRepo.find({ + where: { + lastUpdatedAt: MoreThanOrEqual(windowStart), + orgRevision: { + orgRevisionIsCurrent: true, + }, + }, + relations: [ + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], + select: { + id: true, + current_holderId: true, + lastUpdatedAt: true, + orgRevision: { id: true }, + orgRoot: { ancestorDNA: true }, + orgChild1: { ancestorDNA: true }, + orgChild2: { ancestorDNA: true }, + orgChild3: { ancestorDNA: true }, + orgChild4: { ancestorDNA: true }, + current_holder: { id: true }, + }, + }), + ]); + + console.log("cronjobUpdateOrg: Database query completed", { + posMastersCount: posMasters.length, + employeePosCount: posMasterEmployee.length, + totalRecords: posMasters.length + posMasterEmployee.length, + }); + + // Build payloads with proper profile type tracking + const payloads = this.buildPayloads(posMasters, posMasterEmployee); + + if (payloads.length === 0) { + console.log("cronjobUpdateOrg: No records to process"); + return new HttpSuccess({ + message: "No records to process", + processed: 0, + }); + } + + // Update profile's org structure in leave service by calling API + console.log("cronjobUpdateOrg: Calling leave service API", { + payloadCount: payloads.length, + }); + + await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, { + headers: { + "Content-Type": "application/json", + api_key: process.env.API_KEY, + }, + timeout: 30000, // 30 second timeout + }); + + console.log("cronjobUpdateOrg: Leave service API call successful"); + + // Group profile IDs by type for proper syncing + const profileIdsByType = this.groupProfileIdsByType(payloads); + + // Sync to Keycloak with batching + const keycloakSyncController = new KeycloakSyncController(); + const syncResults = { + total: 0, + success: 0, + failed: 0, + byType: {} as Record, + }; + + // Process each profile type separately + for (const [profileType, profileIds] of Object.entries(profileIdsByType)) { + console.log(`cronjobUpdateOrg: Syncing ${profileType} profiles`, { + count: profileIds.length, + }); + + const batches = this.chunkArray(profileIds, this.BATCH_SIZE); + const typeResult = { total: profileIds.length, success: 0, failed: 0 }; + + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + console.log( + `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`, + { + batchSize: batch.length, + batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min( + (i + 1) * this.BATCH_SIZE, + profileIds.length, + )}`, + }, + ); + + try { + const batchResult: any = await keycloakSyncController.syncByProfileIds({ + profileIds: batch, + profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", + }); + + // Extract result data if available + const resultData = (batchResult as any)?.data || batchResult; + typeResult.success += resultData.success || 0; + typeResult.failed += resultData.failed || 0; + + console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { + success: resultData.success || 0, + failed: resultData.failed || 0, + }); + } catch (error: any) { + console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { + error: error.message, + batchSize: batch.length, + }); + // Count all profiles in failed batch as failed + typeResult.failed += batch.length; + } + } + + syncResults.byType[profileType] = typeResult; + syncResults.total += typeResult.total; + syncResults.success += typeResult.success; + syncResults.failed += typeResult.failed; + } + + const duration = Date.now() - startTime; + console.log("cronjobUpdateOrg: Job completed", { + duration: `${duration}ms`, + processed: payloads.length, + syncResults, + }); + + return new HttpSuccess({ + message: "Update org completed", + processed: payloads.length, + syncResults, + duration: `${duration}ms`, + }); + } catch (error: any) { + const duration = Date.now() - startTime; + console.error("cronjobUpdateOrg: Job failed", { + duration: `${duration}ms`, + error: error.message, + stack: error.stack, + }); + throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"); + } finally { + this.isRunning = false; + } + } + + /** + * Build payloads from PosMaster and EmployeePosMaster records + * Includes proper profile type tracking for accurate Keycloak sync + */ + private buildPayloads( + posMasters: PosMaster[], + posMasterEmployee: EmployeePosMaster[], + ): OrgUpdatePayload[] { + const payloads: OrgUpdatePayload[] = []; + + // Process PosMaster records (PROFILE type) + for (const posMaster of posMasters) { + if (posMaster.current_holder && posMaster.current_holderId) { + payloads.push({ + profileId: posMaster.current_holderId, + rootDnaId: posMaster.orgRoot?.ancestorDNA || null, + child1DnaId: posMaster.orgChild1?.ancestorDNA || null, + child2DnaId: posMaster.orgChild2?.ancestorDNA || null, + child3DnaId: posMaster.orgChild3?.ancestorDNA || null, + child4DnaId: posMaster.orgChild4?.ancestorDNA || null, + profileType: "PROFILE", + }); + } + } + + // Process EmployeePosMaster records (PROFILE_EMPLOYEE type) + for (const employeePos of posMasterEmployee) { + if (employeePos.current_holder && employeePos.current_holderId) { + payloads.push({ + profileId: employeePos.current_holderId, + rootDnaId: employeePos.orgRoot?.ancestorDNA || null, + child1DnaId: employeePos.orgChild1?.ancestorDNA || null, + child2DnaId: employeePos.orgChild2?.ancestorDNA || null, + child3DnaId: employeePos.orgChild3?.ancestorDNA || null, + child4DnaId: employeePos.orgChild4?.ancestorDNA || null, + profileType: "PROFILE_EMPLOYEE", + }); + } + } + + return payloads; + } + + /** + * Group profile IDs by their type for separate Keycloak sync calls + */ + private groupProfileIdsByType(payloads: OrgUpdatePayload[]): Record { + const grouped: Record = { + PROFILE: [], + PROFILE_EMPLOYEE: [], + }; + + for (const payload of payloads) { + grouped[payload.profileType].push(payload.profileId); + } + + // Remove empty groups and deduplicate IDs within each group + const result: Record = {}; + for (const [type, ids] of Object.entries(grouped)) { + if (ids.length > 0) { + // Deduplicate while preserving order + result[type] = Array.from(new Set(ids)); + } + } + + return result; + } + + /** + * Split array into chunks of specified size + */ + private chunkArray(array: T[], chunkSize: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + return chunks; + } +} diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index d977c42d..835e31f2 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -4,8 +4,8 @@ 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; const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET; -const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; -const API_KEY = process.env.API_KEY; +// const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; +// const API_KEY = process.env.API_KEY; let token: string | null = null; let decoded: DecodedJwt | null = null; @@ -165,16 +165,119 @@ export async function getUserList(first = "", max = "", search = "") { if (!res) return false; if (!res.ok) { - return Boolean(console.error("Keycloak Error Response: ", await res.json())); + const errorText = await res.text(); + return Boolean(console.error("Keycloak Error Response: ", errorText)); } - return ((await res.json()) as any[]).map((v: Record) => ({ + // Get raw text first to handle potential JSON parsing errors + const rawText = await res.text(); + + // Log response size for debugging + console.log(`[getUserList] Response size: ${rawText.length} bytes`); + + try { + const data = JSON.parse(rawText) as any[]; + return data.map((v: Record) => ({ + id: v.id, + username: v.username, + firstName: v.firstName, + lastName: v.lastName, + email: v.email, + enabled: v.enabled, + })); + } catch (error) { + console.error(`[getUserList] Failed to parse JSON response:`); + console.error(`[getUserList] Response preview (first 500 chars):`, rawText.substring(0, 500)); + console.error(`[getUserList] Response preview (last 200 chars):`, rawText.slice(-200)); + throw new Error( + `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, + ); + } +} + +/** + * Get all keycloak users with pagination to avoid response size limits + * + * Client must have permission to manage realm's user + * + * @returns user list if success, false otherwise. + */ +export async function getAllUsersPaginated( + search: string = "", + batchSize: number = 100, +): Promise< + | Array<{ + id: string; + username: string; + firstName?: string; + lastName?: string; + email?: string; + enabled: boolean; + }> + | false +> { + const allUsers: any[] = []; + let first = 0; + let hasMore = true; + + while (hasMore) { + const res = await fetch( + `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first}&max=${batchSize}${search ? `&search=${search}` : ""}`, + { + headers: { + authorization: `Bearer ${await getToken()}`, + "content-type": `application/json`, + }, + }, + ).catch((e) => console.log("Keycloak Error: ", e)); + + if (!res) return false; + if (!res.ok) { + const errorText = await res.text(); + console.error("Keycloak Error Response: ", errorText); + return false; + } + + const rawText = await res.text(); + + try { + const batch = JSON.parse(rawText) as any[]; + + if (batch.length === 0) { + hasMore = false; + } else { + allUsers.push(...batch); + first += batch.length; + hasMore = batch.length === batchSize; + + // Log progress for large datasets + if (allUsers.length % 500 === 0) { + console.log(`[getAllUsersPaginated] Fetched ${allUsers.length} users so far...`); + } + } + } catch (error) { + console.error(`[getAllUsersPaginated] Failed to parse JSON response at offset ${first}:`); + console.error( + `[getAllUsersPaginated] Response preview (first 500 chars):`, + rawText.substring(0, 500), + ); + console.error( + `[getAllUsersPaginated] Response preview (last 200 chars):`, + rawText.slice(-200), + ); + throw new Error(`Failed to parse Keycloak response as JSON at batch starting at ${first}.`); + } + } + + console.log(`[getAllUsersPaginated] Total users fetched: ${allUsers.length}`); + + return allUsers.map((v: any) => ({ id: v.id, username: v.username, firstName: v.firstName, lastName: v.lastName, email: v.email, - enabled: v.enabled, + enabled: v.enabled === true || v.enabled === "true", })); } @@ -220,17 +323,34 @@ export async function getUserListOrg(first = "", max = "", search = "", userIds: if (!res) return false; if (!res.ok) { - return Boolean(console.error("Keycloak Error Response: ", await res.json())); + const errorText = await res.text(); + return Boolean(console.error("Keycloak Error Response: ", errorText)); } - return ((await res.json()) as any[]).map((v: Record) => ({ - id: v.id, - username: v.username, - firstName: v.firstName, - lastName: v.lastName, - email: v.email, - enabled: v.enabled, - })); + // Get raw text first to handle potential JSON parsing errors + const rawText = await res.text(); + + try { + const data = JSON.parse(rawText) as any[]; + return data.map((v: Record) => ({ + id: v.id, + username: v.username, + firstName: v.firstName, + lastName: v.lastName, + email: v.email, + enabled: v.enabled, + })); + } catch (error) { + console.error(`[getUserListOrg] Failed to parse JSON response:`); + console.error( + `[getUserListOrg] Response preview (first 500 chars):`, + rawText.substring(0, 500), + ); + console.error(`[getUserListOrg] Response preview (last 200 chars):`, rawText.slice(-200)); + throw new Error( + `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, + ); + } } export async function getUserCountOrg(first = "", max = "", search = "", userIds: string[] = []) { @@ -444,10 +564,12 @@ export async function getRoles(name?: string, token?: string) { })); } - // return { - // id: data.id, - // name: data.name, - // }; + // Return single role object + return { + id: data.id, + name: data.name, + description: data.description, + }; } /** @@ -793,17 +915,20 @@ export async function updateUserAttributes( } // Merge existing attributes with new attributes - // Keycloak requires id to be present in the payload + // IMPORTANT: Spread all existing user fields to preserve firstName, lastName, email, etc. + // The Keycloak PUT endpoint performs a full update, so we must include all fields const updatedAttributes = { - id: existingUser.id, - enabled: existingUser.enabled ?? true, + ...existingUser, attributes: { ...(existingUser.attributes || {}), ...attributes, }, }; - console.log(`[updateUserAttributes] Sending to Keycloak:`, JSON.stringify(updatedAttributes, null, 2)); + console.log( + `[updateUserAttributes] Sending to Keycloak:`, + JSON.stringify(updatedAttributes, null, 2), + ); const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { headers: { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index 80fc9e92..9a571572 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -83,6 +83,7 @@ export async function expressAuthentication( request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? ""; request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? ""; request.app.locals.logData.empType = payload.empType ?? ""; + request.app.locals.logData.prefix = payload.prefix ?? ""; return payload; } diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index 43ac80a3..225f0a37 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -16,6 +16,7 @@ export type RequestWithUser = Request & { orgChild3DnaId?: string; orgChild4DnaId?: string; empType?: string; + prefix?: string; scope?: string; }; }; diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index 3ce04b1e..5b61e286 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -4,7 +4,16 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; // import { PosMaster } from "../entities/PosMaster"; // import { EmployeePosMaster } from "../entities/EmployeePosMaster"; // import { OrgRoot } from "../entities/OrgRoot"; -import { getUser, updateUserAttributes } from "../keycloak"; +import { + createUser, + getUser, + getUserByUsername, + updateUserAttributes, + deleteUser, + getRoles, + addUserRoles, + getAllUsersPaginated, +} from "../keycloak"; import { OrgRevision } from "../entities/OrgRevision"; export interface UserProfileAttributes { @@ -15,6 +24,7 @@ export interface UserProfileAttributes { orgChild3DnaId: string | null; orgChild4DnaId: string | null; empType: string | null; + prefix?: string | null; } /** @@ -73,6 +83,7 @@ export class KeycloakAttributeService { orgChild3DnaId, orgChild4DnaId, empType: "OFFICER", + prefix: profileResult.prefix, }; } @@ -107,6 +118,7 @@ export class KeycloakAttributeService { orgChild3DnaId, orgChild4DnaId, empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, }; } @@ -119,6 +131,7 @@ export class KeycloakAttributeService { orgChild3DnaId: null, orgChild4DnaId: null, empType: null, + prefix: null, }; } @@ -170,6 +183,7 @@ export class KeycloakAttributeService { orgChild3DnaId, orgChild4DnaId, empType: "OFFICER", + prefix: profileResult.prefix, }; } } else { @@ -204,6 +218,7 @@ export class KeycloakAttributeService { orgChild3DnaId, orgChild4DnaId, empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, }; } } @@ -216,6 +231,7 @@ export class KeycloakAttributeService { orgChild3DnaId: null, orgChild4DnaId: null, empType: null, + prefix: null, }; } @@ -243,6 +259,7 @@ export class KeycloakAttributeService { orgChild3DnaId: [attributes.orgChild3DnaId || ""], orgChild4DnaId: [attributes.orgChild4DnaId || ""], empType: [attributes.empType || ""], + prefix: [attributes.prefix || ""], }; const success = await updateUserAttributes(keycloakUserId, keycloakAttributes); @@ -297,15 +314,19 @@ export class KeycloakAttributeService { } /** - * Batch sync multiple users + * Batch sync multiple users with unlimited count and parallel processing * Useful for initial sync or periodic updates * - * @param limit - Maximum number of users to sync (default: 100) + * @param options - Optional configuration (limit for testing, concurrency for parallel processing) * @returns Object with success count and details */ - async batchSyncUsers( - limit: number = 100, - ): Promise<{ total: number; success: number; failed: number; details: any[] }> { + async batchSyncUsers(options?: { + limit?: number; + concurrency?: number; + }): Promise<{ total: number; success: number; failed: number; details: any[] }> { + const limit = options?.limit; + const concurrency = options?.concurrency ?? 5; + const result = { total: 0, success: 0, @@ -314,57 +335,92 @@ export class KeycloakAttributeService { }; try { - // Get profiles with keycloak IDs (ข้าราชการ) - const profiles = await this.profileRepo + // Build query for profiles with keycloak IDs (ข้าราชการ) + const profileQuery = this.profileRepo .createQueryBuilder("p") .where("p.keycloak IS NOT NULL") - .andWhere("p.keycloak != :empty", { empty: "" }) - .take(limit) - .getMany(); + .andWhere("p.keycloak != :empty", { empty: "" }); - // Get profileEmployees with keycloak IDs (ลูกจ้าง) - const profileEmployees = await this.profileEmployeeRepo + // Build query for profileEmployees with keycloak IDs (ลูกจ้าง) + const profileEmployeeQuery = this.profileEmployeeRepo .createQueryBuilder("pe") .where("pe.keycloak IS NOT NULL") - .andWhere("pe.keycloak != :empty", { empty: "" }) - .take(limit) - .getMany(); + .andWhere("pe.keycloak != :empty", { empty: "" }); + + // Apply limit if specified (for testing purposes) + if (limit !== undefined) { + profileQuery.take(limit); + profileEmployeeQuery.take(limit); + } + + // Get profiles from both tables + const [profiles, profileEmployees] = await Promise.all([ + profileQuery.getMany(), + profileEmployeeQuery.getMany(), + ]); + + const allProfiles = [ + ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), + ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), + ]; - const allProfiles = [...profiles, ...profileEmployees]; result.total = allProfiles.length; - for (const profile of allProfiles) { - const keycloakUserId = profile.keycloak; - const profileType = profile instanceof Profile ? "PROFILE" : "PROFILE_EMPLOYEE"; + // Process in parallel with concurrency limit + const processedResults = await this.processInParallel( + allProfiles, + concurrency, + async ({ profile, type }, _index) => { + const keycloakUserId = profile.keycloak; - try { - const success = await this.syncOnOrganizationChange(profile.id, profileType); - if (success) { - result.success++; - result.details.push({ - profileId: profile.id, - keycloakUserId, - status: "success", - }); - } else { + try { + const success = await this.syncOnOrganizationChange(profile.id, type); + if (success) { + result.success++; + return { + profileId: profile.id, + keycloakUserId, + status: "success", + }; + } else { + result.failed++; + return { + profileId: profile.id, + keycloakUserId, + status: "failed", + error: "Sync returned false", + }; + } + } catch (error: any) { result.failed++; - result.details.push({ + return { profileId: profile.id, keycloakUserId, - status: "failed", - error: "Sync returned false", - }); + status: "error", + error: error.message, + }; } - } catch (error: any) { + }, + ); + + // Separate results from errors + for (const resultItem of processedResults) { + if ("error" in resultItem) { result.failed++; result.details.push({ - profileId: profile.id, - keycloakUserId, + profileId: "unknown", + keycloakUserId: "unknown", status: "error", - error: error.message, + error: JSON.stringify(resultItem.error), }); + } else { + result.details.push(resultItem); } } + + console.log( + `Batch sync completed: total=${result.total}, success=${result.success}, failed=${result.failed}`, + ); } catch (error) { console.error("Error in batch sync:", error); } @@ -396,10 +452,477 @@ export class KeycloakAttributeService { orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "", orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "", empType: user.attributes.empType?.[0] || "", + prefix: user.attributes.prefix?.[0] || "", }; } catch (error) { console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error); return null; } } + + /** + * Ensure Keycloak user exists for a profile + * Creates user if keycloak field is empty OR if stored keycloak ID doesn't exist in Keycloak + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' + * @returns Object with status and details + */ + async ensureKeycloakUser( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise<{ + success: boolean; + action: "created" | "verified" | "skipped" | "error"; + keycloakUserId?: string; + error?: string; + }> { + try { + // Get profile from database + let profile: Profile | ProfileEmployee | null = null; + + if (profileType === "PROFILE") { + profile = await this.profileRepo.findOne({ where: { id: profileId } }); + } else { + profile = await this.profileEmployeeRepo.findOne({ where: { id: profileId } }); + } + + if (!profile) { + return { + success: false, + action: "error", + error: `Profile ${profileId} not found in database`, + }; + } + + // Check if citizenId exists + if (!profile.citizenId) { + return { + success: false, + action: "skipped", + error: "No citizenId found", + }; + } + + // Case 1: keycloak field is empty -> create new user + if (!profile.keycloak || profile.keycloak.trim() === "") { + const result = await this.createKeycloakUserFromProfile(profile, profileType); + return result; + } + + // Case 2: keycloak field is not empty -> verify user exists in Keycloak + const existingUser = await getUser(profile.keycloak); + + if (!existingUser) { + // User doesn't exist in Keycloak, create new one + console.log( + `Keycloak user ${profile.keycloak} not found in Keycloak, creating new user for profile ${profileId}`, + ); + const result = await this.createKeycloakUserFromProfile(profile, profileType); + return result; + } + + // User exists in Keycloak, verified + return { + success: true, + action: "verified", + keycloakUserId: profile.keycloak, + }; + } catch (error: any) { + console.error(`Error ensuring Keycloak user for profile ${profileId}:`, error); + return { + success: false, + action: "error", + error: error.message || "Unknown error", + }; + } + } + + /** + * Create Keycloak user from profile data + * + * @param profile - Profile or ProfileEmployee entity + * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' + * @returns Object with status and details + */ + private async createKeycloakUserFromProfile( + profile: Profile | ProfileEmployee, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise<{ + success: boolean; + action: "created" | "verified" | "skipped" | "error"; + keycloakUserId?: string; + error?: string; + }> { + try { + // Check if user already exists by username (citizenId) + const existingUserByUsername = await getUserByUsername(profile.citizenId); + if (Array.isArray(existingUserByUsername) && existingUserByUsername.length > 0) { + // User already exists with this username, update the keycloak field + const existingUserId = existingUserByUsername[0].id; + console.log( + `User with citizenId ${profile.citizenId} already exists in Keycloak with ID ${existingUserId}`, + ); + + // Update the keycloak field in database + if (profileType === "PROFILE") { + await this.profileRepo.update(profile.id, { keycloak: existingUserId }); + } else { + await this.profileEmployeeRepo.update(profile.id, { keycloak: existingUserId }); + } + + // Assign default USER role to existing user + const userRole = await getRoles("USER"); + if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { + const roleAssigned = await addUserRoles(existingUserId, [ + { id: String(userRole.id), name: String(userRole.name) }, + ]); + if (roleAssigned) { + console.log(`Assigned USER role to existing user ${existingUserId}`); + } else { + console.warn(`Failed to assign USER role to existing user ${existingUserId}`); + } + } else { + console.warn(`USER role not found in Keycloak`); + } + + return { + success: true, + action: "verified", + keycloakUserId: existingUserId, + }; + } + + // Create new user in Keycloak + const createResult = await createUser(profile.citizenId, "P@ssw0rd", { + firstName: profile.firstName || "", + lastName: profile.lastName || "", + email: profile.email || undefined, + enabled: true, + }); + + if (!createResult || typeof createResult !== "string") { + return { + success: false, + action: "error", + error: "Failed to create user in Keycloak", + }; + } + + const keycloakUserId = createResult; + + // Update the keycloak field in database + if (profileType === "PROFILE") { + await this.profileRepo.update(profile.id, { keycloak: keycloakUserId }); + } else { + await this.profileEmployeeRepo.update(profile.id, { keycloak: keycloakUserId }); + } + + // Assign default USER role + const userRole = await getRoles("USER"); + if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { + const roleAssigned = await addUserRoles(keycloakUserId, [ + { id: String(userRole.id), name: String(userRole.name) }, + ]); + if (roleAssigned) { + console.log(`Assigned USER role to user ${keycloakUserId}`); + } else { + console.warn(`Failed to assign USER role to user ${keycloakUserId}`); + } + } else { + console.warn(`USER role not found in Keycloak`); + } + + console.log( + `Created Keycloak user for profile ${profile.id} (citizenId: ${profile.citizenId}) with ID ${keycloakUserId}`, + ); + + return { + success: true, + action: "created", + keycloakUserId, + }; + } catch (error: any) { + console.error(`Error creating Keycloak user for profile ${profile.id}:`, error); + return { + success: false, + action: "error", + error: error.message || "Unknown error", + }; + } + } + + /** + * Process items in parallel with concurrency limit + */ + private async processInParallel( + items: T[], + concurrencyLimit: number, + processor: (item: T, index: number) => Promise, + ): Promise> { + const results: Array = []; + + // Process items in batches + for (let i = 0; i < items.length; i += concurrencyLimit) { + const batch = items.slice(i, i + concurrencyLimit); + + // Process batch in parallel with error handling + const batchResults = await Promise.all( + batch.map(async (item, batchIndex) => { + try { + return await processor(item, i + batchIndex); + } catch (error) { + return { error }; + } + }), + ); + + results.push(...batchResults); + + // Log progress after each batch + const completed = Math.min(i + concurrencyLimit, items.length); + console.log(`Progress: ${completed}/${items.length}`); + } + + return results; + } + + /** + * Batch ensure Keycloak users for all profiles + * Processes all rows in Profile and ProfileEmployee tables + * + * @returns Object with total, success, failed counts and details + */ + async batchEnsureKeycloakUsers(): Promise<{ + total: number; + created: number; + verified: number; + skipped: number; + failed: number; + details: Array<{ + profileId: string; + profileType: string; + action: string; + keycloakUserId?: string; + error?: string; + }>; + }> { + const result = { + total: 0, + created: 0, + verified: 0, + skipped: 0, + failed: 0, + details: [] as Array<{ + profileId: string; + profileType: string; + action: string; + keycloakUserId?: string; + error?: string; + }>, + }; + + try { + // Get all profiles from Profile table (ข้าราชการ) + const profiles = await this.profileRepo.find({ where: { isLeave: false } }); // Only active profiles + + // Get all profiles from ProfileEmployee table (ลูกจ้าง) + const profileEmployees = await this.profileEmployeeRepo.find({ where: { isLeave: false } }); // Only active profiles + + const allProfiles = [ + ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), + ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), + ]; + + result.total = allProfiles.length; + + // Process in parallel with concurrency limit + const CONCURRENCY_LIMIT = 5; // Adjust based on environment + + const processedResults = await this.processInParallel( + allProfiles, + CONCURRENCY_LIMIT, + async ({ profile, type }) => { + const ensureResult = await this.ensureKeycloakUser(profile.id, type); + + // Update counters + switch (ensureResult.action) { + case "created": + result.created++; + break; + case "verified": + result.verified++; + break; + case "skipped": + result.skipped++; + break; + case "error": + result.failed++; + break; + } + + return { + profileId: profile.id, + profileType: type, + action: ensureResult.action, + keycloakUserId: ensureResult.keycloakUserId, + error: ensureResult.error, + }; + }, + ); + + // Separate results from errors + for (const resultItem of processedResults) { + if ("error" in resultItem) { + result.failed++; + result.details.push({ + profileId: "unknown", + profileType: "unknown", + action: "error", + error: JSON.stringify(resultItem.error), + }); + } else { + result.details.push(resultItem); + } + } + + console.log( + `Batch ensure Keycloak users completed: total=${result.total}, created=${result.created}, verified=${result.verified}, skipped=${result.skipped}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in batch ensure Keycloak users:", error); + } + + return result; + } + + /** + * Clear orphaned Keycloak users + * Deletes users in Keycloak that are not referenced in Profile or ProfileEmployee tables + * + * @param skipUsernames - Array of usernames to skip (e.g., ['super_admin']) + * @returns Object with counts and details + */ + async clearOrphanedKeycloakUsers(skipUsernames: string[] = []): Promise<{ + totalInKeycloak: number; + totalInDatabase: number; + orphanedCount: number; + deleted: number; + skipped: number; + failed: number; + details: Array<{ + keycloakUserId: string; + username: string; + action: "deleted" | "skipped" | "error"; + error?: string; + }>; + }> { + const result = { + totalInKeycloak: 0, + totalInDatabase: 0, + orphanedCount: 0, + deleted: 0, + skipped: 0, + failed: 0, + details: [] as Array<{ + keycloakUserId: string; + username: string; + action: "deleted" | "skipped" | "error"; + error?: string; + }>, + }; + + try { + // Get all keycloak IDs from database (Profile + ProfileEmployee) + const profiles = await this.profileRepo + .createQueryBuilder("p") + .where("p.keycloak IS NOT NULL") + .andWhere("p.keycloak != :empty", { empty: "" }) + .getMany(); + + const profileEmployees = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .where("pe.keycloak IS NOT NULL") + .andWhere("pe.keycloak != :empty", { empty: "" }) + .getMany(); + + // Create a Set of all keycloak IDs in database for O(1) lookup + const databaseKeycloakIds = new Set(); + for (const p of profiles) { + if (p.keycloak) databaseKeycloakIds.add(p.keycloak); + } + for (const pe of profileEmployees) { + if (pe.keycloak) databaseKeycloakIds.add(pe.keycloak); + } + + result.totalInDatabase = databaseKeycloakIds.size; + + // Get all users from Keycloak with pagination to avoid response size limits + const keycloakUsers = await getAllUsersPaginated(); + if (!keycloakUsers || typeof keycloakUsers !== "object") { + throw new Error("Failed to get users from Keycloak"); + } + + result.totalInKeycloak = keycloakUsers.length; + + // Find orphaned users (in Keycloak but not in database) + const orphanedUsers = keycloakUsers.filter((user: any) => !databaseKeycloakIds.has(user.id)); + result.orphanedCount = orphanedUsers.length; + + // Delete orphaned users (skip protected ones) + for (const user of orphanedUsers) { + const username = user.username; + const userId = user.id; + + // Check if user should be skipped + if (skipUsernames.includes(username)) { + result.skipped++; + result.details.push({ + keycloakUserId: userId, + username, + action: "skipped", + }); + continue; + } + + // Delete user from Keycloak + try { + const deleteSuccess = await deleteUser(userId); + if (deleteSuccess) { + result.deleted++; + result.details.push({ + keycloakUserId: userId, + username, + action: "deleted", + }); + } else { + result.failed++; + result.details.push({ + keycloakUserId: userId, + username, + action: "error", + error: "Failed to delete user from Keycloak", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ + keycloakUserId: userId, + username, + action: "error", + error: error.message || "Unknown error", + }); + } + } + + console.log( + `Clear orphaned Keycloak users completed: totalInKeycloak=${result.totalInKeycloak}, totalInDatabase=${result.totalInDatabase}, orphaned=${result.orphanedCount}, deleted=${result.deleted}, skipped=${result.skipped}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in clear orphaned Keycloak users:", error); + throw error; + } + + return result; + } } From 26bcfd728e5bb9db3a0c7335aff9d9dd9887e824 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 21:47:10 +0700 Subject: [PATCH 118/178] fix: handle error --- src/controllers/PermissionController.ts | 105 +++++++++++++++++++----- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 801d4b97..065bc3bf 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -34,10 +34,18 @@ export class PermissionController extends Controller { @Get("") public async getPermission(@Request() request: RequestWithUser) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: parseInt(REDIS_PORT as string) || 6379, + }, }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ @@ -113,6 +121,7 @@ export class PermissionController extends Controller { roles: roleAttrData, }; redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); + await redisClient.quit(); } return new HttpSuccess(reply); } @@ -126,10 +135,18 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: parseInt(REDIS_PORT as string) || 6379, + }, }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; @@ -229,6 +246,7 @@ export class PermissionController extends Controller { }); redisClient.setex("menu_" + profile.id, 86400, JSON.stringify(reply)); + await redisClient.quit(); } return new HttpSuccess(reply); @@ -307,10 +325,18 @@ export class PermissionController extends Controller { @Path() system: string, @Path() action: string, ) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: parseInt(REDIS_PORT as string) || 6379, + }, }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; @@ -398,6 +424,7 @@ export class PermissionController extends Controller { redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply)); } } + await redisClient.quit(); return new HttpSuccess(reply); } @@ -416,10 +443,18 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: parseInt(REDIS_PORT as string) || 6379, + }, }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let org = this.PermissionOrg(request, system, action); @@ -499,15 +534,24 @@ export class PermissionController extends Controller { redisClient.setex("user_" + profile.id, 86400, JSON.stringify(reply)); } } + await redisClient.quit(); return new HttpSuccess(reply); } public async getPermissionFunc(@Request() request: RequestWithUser) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: parseInt(REDIS_PORT as string) || 6379, + }, }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ @@ -583,6 +627,7 @@ export class PermissionController extends Controller { roles: roleAttrData, }; redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); + await redisClient.quit(); } return reply; } @@ -610,10 +655,18 @@ export class PermissionController extends Controller { } public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: parseInt(REDIS_PORT as string) || 6379, + }, }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; @@ -700,6 +753,7 @@ export class PermissionController extends Controller { redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply)); } } + await redisClient.quit(); return reply; } @@ -782,10 +836,18 @@ export class PermissionController extends Controller { @Get("checkOrg/{keycloakId}") public async checkOrg(@Path() keycloakId: string) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, + const redisClient = this.redis.createClient({ + socket: { + host: REDIS_HOST, + port: parseInt(REDIS_PORT as string) || 6379, + }, }); + + redisClient.on("error", (err: any) => { + console.error("[REDIS] Connection error:", err.message); + }); + + await redisClient.connect(); // const getAsync = promisify(redisClient.get).bind(redisClient); // let profileType = "OFFICER"; @@ -836,6 +898,7 @@ export class PermissionController extends Controller { }; } redisClient.setex("org_" + profile.id, 86400, JSON.stringify(reply)); //Create Redis + await redisClient.quit(); // } else { // const posMaster = await this.posMasterEmpRepository.findOne({ // where: { From cae5aeae479eb43717d0bcc07f303c8d7c61f793 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 21:55:51 +0700 Subject: [PATCH 119/178] test --- src/app.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app.ts b/src/app.ts index 06f76548..c0ee5b1a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,7 +15,7 @@ import { logMemoryStore } from "./utils/LogMemoryStore"; import { orgStructureCache } from "./utils/OrgStructureCache"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; -import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; +// import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; @@ -87,15 +87,15 @@ async function main() { }); // Cron job for updating org DNA - every day at 03:00:00 - const cronTime_UpdateOrg = "0 0 3 * * *"; - cron.schedule(cronTime_UpdateOrg, async () => { - try { - const scriptProfileOrgController = new ScriptProfileOrgController(); - await scriptProfileOrgController.cronjobUpdateOrg({} as any); - } catch (error) { - console.error("Error executing cronjobUpdateOrg:", error); - } - }); + // const cronTime_UpdateOrg = "0 0 3 * * *"; + // cron.schedule(cronTime_UpdateOrg, async () => { + // try { + // const scriptProfileOrgController = new ScriptProfileOrgController(); + // await scriptProfileOrgController.cronjobUpdateOrg({} as any); + // } catch (error) { + // console.error("Error executing cronjobUpdateOrg:", error); + // } + // }); // Cron job for updating tenure - every day at 04:00:00 const cronTime_Tenure = "0 0 4 * * *"; From f1f4717b5becd37c7d4f2b71e5f0b8d82a63dc67 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 22:04:02 +0700 Subject: [PATCH 120/178] fix --- src/__tests__/setup.ts | 17 - src/__tests__/unit/OrgMapping.spec.ts | 54 -- .../unit/OrganizationController.spec.ts | 460 ------------------ src/app.ts | 20 +- 4 files changed, 10 insertions(+), 541 deletions(-) delete mode 100644 src/__tests__/setup.ts delete mode 100644 src/__tests__/unit/OrgMapping.spec.ts delete mode 100644 src/__tests__/unit/OrganizationController.spec.ts diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts deleted file mode 100644 index 04ef89ef..00000000 --- a/src/__tests__/setup.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Test setup file for Jest -// Mock environment variables -process.env.NODE_ENV = 'test'; -process.env.DB_HOST = 'localhost'; -process.env.DB_PORT = '3306'; -process.env.DB_USERNAME = 'test'; -process.env.DB_PASSWORD = 'test'; -process.env.DB_DATABASE = 'test_db'; - -// Mock console methods to reduce noise in tests -global.console = { - ...console, - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - log: jest.fn(), -}; diff --git a/src/__tests__/unit/OrgMapping.spec.ts b/src/__tests__/unit/OrgMapping.spec.ts deleted file mode 100644 index c59e5697..00000000 --- a/src/__tests__/unit/OrgMapping.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Unit tests for move-draft-to-current helper functions - */ - -import { OrgIdMapping, AllOrgMappings } from '../../interfaces/OrgMapping'; - -// Mock dependencies -jest.mock('../../database/data-source', () => ({ - AppDataSource: { - createQueryRunner: jest.fn(), - }, -})); - -describe('OrgMapping Interfaces', () => { - describe('OrgIdMapping', () => { - it('should create a valid OrgIdMapping', () => { - const mapping: OrgIdMapping = { - byAncestorDNA: new Map(), - byDraftId: new Map(), - }; - - expect(mapping.byAncestorDNA).toBeInstanceOf(Map); - expect(mapping.byDraftId).toBeInstanceOf(Map); - }); - - it('should store and retrieve values correctly', () => { - const mapping: OrgIdMapping = { - byAncestorDNA: new Map([['dna1', 'id1']]), - byDraftId: new Map([['draftId1', 'currentId1']]), - }; - - expect(mapping.byAncestorDNA.get('dna1')).toBe('id1'); - expect(mapping.byDraftId.get('draftId1')).toBe('currentId1'); - }); - }); - - describe('AllOrgMappings', () => { - it('should create a valid AllOrgMappings', () => { - const mappings: AllOrgMappings = { - orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, - orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, - }; - - expect(mappings.orgRoot).toBeDefined(); - expect(mappings.orgChild1).toBeDefined(); - expect(mappings.orgChild2).toBeDefined(); - expect(mappings.orgChild3).toBeDefined(); - expect(mappings.orgChild4).toBeDefined(); - }); - }); -}); diff --git a/src/__tests__/unit/OrganizationController.spec.ts b/src/__tests__/unit/OrganizationController.spec.ts deleted file mode 100644 index 615298bc..00000000 --- a/src/__tests__/unit/OrganizationController.spec.ts +++ /dev/null @@ -1,460 +0,0 @@ -/** - * Unit tests for OrganizationController move-draft-to-current helper functions - */ - -import { OrgIdMapping } from '../../interfaces/OrgMapping'; - -// Mock typeorm -jest.mock('typeorm', () => ({ - Entity: jest.fn(), - Column: jest.fn(), - ManyToOne: jest.fn(), - JoinColumn: jest.fn(), - OneToMany: jest.fn(), - In: jest.fn((val: any) => val), - Like: jest.fn((val: any) => val), - IsNull: jest.fn(), - Not: jest.fn(), -})); - -// Mock entities -jest.mock('../../entities/OrgRoot', () => ({ OrgRoot: {} })); -jest.mock('../../entities/OrgChild1', () => ({ OrgChild1: {} })); -jest.mock('../../entities/OrgChild2', () => ({ OrgChild2: {} })); -jest.mock('../../entities/OrgChild3', () => ({ OrgChild3: {} })); -jest.mock('../../entities/OrgChild4', () => ({ OrgChild4: {} })); -jest.mock('../../entities/PosMaster', () => ({ PosMaster: {} })); -jest.mock('../../entities/Position', () => ({ Position: {} })); - -// Import after mocking -import { In, Like } from 'typeorm'; -import { OrgRoot } from '../../entities/OrgRoot'; -import { OrgChild1 } from '../../entities/OrgChild1'; -import { OrgChild2 } from '../../entities/OrgChild2'; -import { OrgChild3 } from '../../entities/OrgChild3'; -import { OrgChild4 } from '../../entities/OrgChild4'; -import { PosMaster } from '../../entities/PosMaster'; -import { Position } from '../../entities/Position'; - -describe('OrganizationController - Helper Functions', () => { - let mockQueryRunner: any; - let mockController: any; - - beforeEach(() => { - // Mock queryRunner - mockQueryRunner = { - manager: { - find: jest.fn(), - delete: jest.fn(), - update: jest.fn(), - create: jest.fn(), - save: jest.fn(), - }, - }; - - // Import the controller class (we'll need to mock the private methods) - // Since we're testing private methods, we'll create a test class - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('resolveOrgId()', () => { - it('should return null when draftId is null', () => { - const mapping: OrgIdMapping = { - byAncestorDNA: new Map(), - byDraftId: new Map(), - }; - - // Simulate the function logic - const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { - if (!draftId) return null; - return mapping.byDraftId.get(draftId) ?? null; - }; - - expect(resolveOrgId(null, mapping)).toBeNull(); - }); - - it('should return null when draftId is undefined', () => { - const mapping: OrgIdMapping = { - byAncestorDNA: new Map(), - byDraftId: new Map(), - }; - - const resolveOrgId = (draftId: string | null | undefined, mapping: OrgIdMapping): string | null => { - if (!draftId) return null; - return mapping.byDraftId.get(draftId) ?? null; - }; - - expect(resolveOrgId(undefined, mapping)).toBeNull(); - }); - - it('should return mapped ID when draftId exists in mapping', () => { - const mapping: OrgIdMapping = { - byAncestorDNA: new Map(), - byDraftId: new Map([['draft1', 'current1']]), - }; - - const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { - if (!draftId) return null; - return mapping.byDraftId.get(draftId) ?? null; - }; - - expect(resolveOrgId('draft1', mapping)).toBe('current1'); - }); - - it('should return null when draftId does not exist in mapping', () => { - const mapping: OrgIdMapping = { - byAncestorDNA: new Map(), - byDraftId: new Map(), - }; - - const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { - if (!draftId) return null; - return mapping.byDraftId.get(draftId) ?? null; - }; - - expect(resolveOrgId('nonexistent', mapping)).toBeNull(); - }); - }); - - describe('cascadeDeletePositions()', () => { - it('should delete positions with orgRootId when entityClass is OrgRoot', async () => { - const node = { - id: 'node1', - orgRevisionId: 'rev1', - }; - - await mockQueryRunner.manager.delete(PosMaster, { - orgRevisionId: 'rev1', - orgRootId: 'node1', - }); - - expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { - orgRevisionId: 'rev1', - orgRootId: 'node1', - }); - }); - - it('should delete positions with orgChild1Id when entityClass is OrgChild1', async () => { - const node = { - id: 'node1', - orgRevisionId: 'rev1', - }; - - await mockQueryRunner.manager.delete(PosMaster, { - orgRevisionId: 'rev1', - orgChild1Id: 'node1', - }); - - expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { - orgRevisionId: 'rev1', - orgChild1Id: 'node1', - }); - }); - - it('should delete positions with orgChild2Id when entityClass is OrgChild2', async () => { - const node = { - id: 'node1', - orgRevisionId: 'rev1', - }; - - await mockQueryRunner.manager.delete(PosMaster, { - orgRevisionId: 'rev1', - orgChild2Id: 'node1', - }); - - expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { - orgRevisionId: 'rev1', - orgChild2Id: 'node1', - }); - }); - - it('should delete positions with orgChild3Id when entityClass is OrgChild3', async () => { - const node = { - id: 'node1', - orgRevisionId: 'rev1', - }; - - await mockQueryRunner.manager.delete(PosMaster, { - orgRevisionId: 'rev1', - orgChild3Id: 'node1', - }); - - expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { - orgRevisionId: 'rev1', - orgChild3Id: 'node1', - }); - }); - - it('should delete positions with orgChild4Id when entityClass is OrgChild4', async () => { - const node = { - id: 'node1', - orgRevisionId: 'rev1', - }; - - await mockQueryRunner.manager.delete(PosMaster, { - orgRevisionId: 'rev1', - orgChild4Id: 'node1', - }); - - expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { - orgRevisionId: 'rev1', - orgChild4Id: 'node1', - }); - }); - }); - - describe('syncOrgLevel()', () => { - beforeEach(() => { - mockQueryRunner.manager.find.mockResolvedValue([]); - mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); - mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); - mockQueryRunner.manager.create.mockReturnValue({}); - mockQueryRunner.manager.save.mockResolvedValue({}); - }); - - it('should fetch draft and current nodes with Like filter', async () => { - const repository = { - find: jest.fn().mockResolvedValue([]), - }; - - await repository.find({ - where: { - orgRevisionId: 'draftRev1', - ancestorDNA: Like('root-dna%'), - }, - }); - - await repository.find({ - where: { - orgRevisionId: 'currentRev1', - ancestorDNA: Like('root-dna%'), - }, - }); - - expect(repository.find).toHaveBeenCalledTimes(2); - }); - - it('should build lookup maps from draft and current nodes', () => { - const draftNodes = [ - { id: 'draft1', ancestorDNA: 'root-dna/child1' }, - { id: 'draft2', ancestorDNA: 'root-dna/child2' }, - ]; - const currentNodes = [ - { id: 'current1', ancestorDNA: 'root-dna/child1' }, - ]; - - const draftByDNA = new Map(draftNodes.map(n => [n.ancestorDNA, n])); - const currentByDNA = new Map(currentNodes.map(n => [n.ancestorDNA, n])); - - expect(draftByDNA.size).toBe(2); - expect(currentByDNA.size).toBe(1); - expect(draftByDNA.get('root-dna/child1')).toEqual(draftNodes[0]); - expect(currentByDNA.get('root-dna/child1')).toEqual(currentNodes[0]); - }); - - it('should identify nodes to delete (in current but not in draft)', () => { - const draftNodes = [ - { id: 'draft1', ancestorDNA: 'root-dna/child1' }, - ]; - const currentNodes = [ - { id: 'current1', ancestorDNA: 'root-dna/child1' }, - { id: 'current2', ancestorDNA: 'root-dna/child2' }, // Not in draft - ]; - - const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n])); - const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA)); - - expect(toDelete).toHaveLength(1); - expect(toDelete[0].id).toBe('current2'); - }); - - it('should identify nodes to update (in both draft and current)', () => { - const draftNodes = [ - { id: 'draft1', ancestorDNA: 'root-dna/child1' }, - { id: 'draft2', ancestorDNA: 'root-dna/child2' }, - ]; - const currentNodes = [ - { id: 'current1', ancestorDNA: 'root-dna/child1' }, - ]; - - const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); - const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA)); - - expect(toUpdate).toHaveLength(1); - expect(toUpdate[0].id).toBe('draft1'); - }); - - it('should identify nodes to insert (in draft but not in current)', () => { - const draftNodes = [ - { id: 'draft1', ancestorDNA: 'root-dna/child1' }, - { id: 'draft2', ancestorDNA: 'root-dna/child2' }, - ]; - const currentNodes = [ - { id: 'current1', ancestorDNA: 'root-dna/child1' }, - ]; - - const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); - const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA)); - - expect(toInsert).toHaveLength(1); - expect(toInsert[0].id).toBe('draft2'); - }); - - it('should return correct mapping after sync', () => { - const mapping: OrgIdMapping = { - byAncestorDNA: new Map([ - ['root-dna/child1', 'current1'], - ['root-dna/child2', 'current2'], - ]), - byDraftId: new Map([ - ['draft1', 'current1'], - ['draft2', 'current2'], - ]), - }; - - expect(mapping.byAncestorDNA.get('root-dna/child1')).toBe('current1'); - expect(mapping.byDraftId.get('draft1')).toBe('current1'); - expect(mapping.byDraftId.get('draft2')).toBe('current2'); - }); - }); - - describe('syncPositionsForPosMaster()', () => { - beforeEach(() => { - mockQueryRunner.manager.find.mockResolvedValue([]); - mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); - mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); - mockQueryRunner.manager.create.mockReturnValue({}); - mockQueryRunner.manager.save.mockResolvedValue({}); - }); - - it('should fetch draft and current positions for a posMaster', async () => { - const draftPosMasterId = 'draft-pos-1'; - const currentPosMasterId = 'current-pos-1'; - - mockQueryRunner.manager.find - .mockResolvedValueOnce([{ id: 'pos1', posMasterId: draftPosMasterId }]) - .mockResolvedValueOnce([{ id: 'pos2', posMasterId: currentPosMasterId }]); - - await mockQueryRunner.manager.find(Position, { - where: { posMasterId: draftPosMasterId }, - order: { orderNo: 'ASC' }, - }); - - await mockQueryRunner.manager.find(Position, { - where: { posMasterId: currentPosMasterId }, - }); - - expect(mockQueryRunner.manager.find).toHaveBeenCalledTimes(2); - }); - - it('should delete all current positions when no draft positions exist', async () => { - const currentPositions = [ - { id: 'pos1', orderNo: 1 }, - { id: 'pos2', orderNo: 2 }, - ]; - - mockQueryRunner.manager.find - .mockResolvedValueOnce([]) // No draft positions - .mockResolvedValueOnce(currentPositions); - - await mockQueryRunner.manager.delete(Position, ['pos1', 'pos2']); - - expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(Position, ['pos1', 'pos2']); - }); - - it('should delete positions not in draft (by orderNo)', async () => { - const draftPositions = [ - { id: 'dpos1', orderNo: 1 }, - { id: 'dpos2', orderNo: 2 }, - ]; - const currentPositions = [ - { id: 'cpos1', orderNo: 1 }, - { id: 'cpos2', orderNo: 2 }, - { id: 'cpos3', orderNo: 3 }, // Not in draft - ]; - - mockQueryRunner.manager.find - .mockResolvedValueOnce(draftPositions) - .mockResolvedValueOnce(currentPositions); - - const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); - const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo)); - - expect(toDelete).toHaveLength(1); - expect(toDelete[0].id).toBe('cpos3'); - }); - - it('should update existing positions (matched by orderNo)', async () => { - const draftPositions = [ - { - id: 'dpos1', - orderNo: 1, - positionName: 'Updated Name', - positionField: 'field1', - posTypeId: 'type1', - posLevelId: 'level1', - }, - ]; - const currentPositions = [ - { id: 'cpos1', orderNo: 1, positionName: 'Old Name' }, - ]; - - const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); - const draftPos = draftPositions[0]; - const current = currentByOrderNo.get(draftPos.orderNo); - - expect(current).toBeDefined(); - - if (current) { - const updateData = { - positionName: draftPos.positionName, - positionField: draftPos.positionField, - posTypeId: draftPos.posTypeId, - posLevelId: draftPos.posLevelId, - }; - - await mockQueryRunner.manager.update(Position, current.id, updateData); - - expect(mockQueryRunner.manager.update).toHaveBeenCalledWith( - Position, - 'cpos1', - expect.objectContaining({ positionName: 'Updated Name' }) - ); - } - }); - - it('should insert new positions not in current', async () => { - const draftPositions = [ - { id: 'dpos1', orderNo: 1, positionName: 'New Position' }, - ]; - const currentPositions: any[] = []; - - const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); - const draftPos = draftPositions[0]; - const current = currentByOrderNo.get(draftPos.orderNo); - const currentPosMasterId = 'current-pos-1'; - - expect(current).toBeUndefined(); - - if (!current) { - const newPosition = { - ...draftPos, - id: undefined, - posMasterId: currentPosMasterId, - }; - - await mockQueryRunner.manager.create(Position, newPosition); - await mockQueryRunner.manager.save(newPosition); - - expect(mockQueryRunner.manager.create).toHaveBeenCalledWith(Position, { - ...draftPos, - id: undefined, - posMasterId: currentPosMasterId, - }); - } - }); - }); -}); diff --git a/src/app.ts b/src/app.ts index c0ee5b1a..06f76548 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,7 +15,7 @@ import { logMemoryStore } from "./utils/LogMemoryStore"; import { orgStructureCache } from "./utils/OrgStructureCache"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; -// import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; +import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; @@ -87,15 +87,15 @@ async function main() { }); // Cron job for updating org DNA - every day at 03:00:00 - // const cronTime_UpdateOrg = "0 0 3 * * *"; - // cron.schedule(cronTime_UpdateOrg, async () => { - // try { - // const scriptProfileOrgController = new ScriptProfileOrgController(); - // await scriptProfileOrgController.cronjobUpdateOrg({} as any); - // } catch (error) { - // console.error("Error executing cronjobUpdateOrg:", error); - // } - // }); + const cronTime_UpdateOrg = "0 0 3 * * *"; + cron.schedule(cronTime_UpdateOrg, async () => { + try { + const scriptProfileOrgController = new ScriptProfileOrgController(); + await scriptProfileOrgController.cronjobUpdateOrg({} as any); + } catch (error) { + console.error("Error executing cronjobUpdateOrg:", error); + } + }); // Cron job for updating tenure - every day at 04:00:00 const cronTime_Tenure = "0 0 4 * * *"; From 693afddc221722f4aeb51b1a5919782e802668ef Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 22:10:33 +0700 Subject: [PATCH 121/178] fix --- tsconfig.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index f5f96a0d..1c974407 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,11 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "resolveJsonModule": true, - "skipLibCheck": true, + "skipLibCheck": true }, + "exclude": [ + "src/__tests__/**", + "**/*.spec.ts", + "**/*.test.ts" + ] } From aad0a03a17368a1112cad374ec4dbef2718304d4 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 22:23:38 +0700 Subject: [PATCH 122/178] fix --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1b119c40..51e444cf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,4 +31,4 @@ USER node # Define the entrypoint and default command # If you have a custom entrypoint script -CMD [ "node", "dist/app.js" ] +CMD [ "node", "dist/src/app.js" ] From 30fd08fc85c32c9a28ddc6c994e02b394c9420a9 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 22:31:30 +0700 Subject: [PATCH 123/178] fix --- src/database/data-source.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/data-source.ts b/src/database/data-source.ts index e44252a1..92b60aa2 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -49,11 +49,11 @@ export const AppDataSource = new DataSource({ entities: process.env.NODE_ENV !== "production" ? ["src/entities/**/*.ts"] - : ["dist/entities/**/*{.ts,.js}"], + : ["dist/entities/**/*.js"], migrations: process.env.NODE_ENV !== "production" ? ["src/migration/**/*.ts"] - : ["dist/migration/**/*{.ts,.js}"], + : ["dist/migration/**/*.js"], subscribers: [], logger: new MyCustomLogger(), // Connection pool settings to prevent connection exhaustion From 1c629cc6e08a5f29497c2f18681495ab68f1a7c0 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 22:37:03 +0700 Subject: [PATCH 124/178] Revert "Merge branch 'feat/keyloak-token-data' into develop" This reverts commit 1b3806c6f7b2dbb0645e403ec297f1cf62c6cf5b, reversing changes made to 79dbba2c89cc6f422d82b75a4d11539cc2da22f9. --- CLAUDE.md | 137 --- scripts/KEYCLOAK_SYNC_README.md | 149 --- scripts/assign-user-role.ts | 269 ----- scripts/clear-orphaned-users.ts | 80 -- scripts/ensure-users.ts | 91 -- scripts/sync-all.ts | 93 -- src/app.ts | 44 +- src/controllers/KeycloakSyncController.ts | 254 ----- src/controllers/ScriptProfileOrgController.ts | 326 ------ src/keycloak/index.ts | 225 +---- src/middlewares/auth.ts | 10 - src/middlewares/user.ts | 9 - src/services/KeycloakAttributeService.ts | 928 ------------------ 13 files changed, 33 insertions(+), 2582 deletions(-) delete mode 100644 CLAUDE.md delete mode 100644 scripts/KEYCLOAK_SYNC_README.md delete mode 100644 scripts/assign-user-role.ts delete mode 100644 scripts/clear-orphaned-users.ts delete mode 100644 scripts/ensure-users.ts delete mode 100644 scripts/sync-all.ts delete mode 100644 src/controllers/KeycloakSyncController.ts delete mode 100644 src/controllers/ScriptProfileOrgController.ts delete mode 100644 src/services/KeycloakAttributeService.ts diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1db8de32..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,137 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is **bma-ehr-organization** - an HRMS (Human Resource Management System) API backend for a Thai healthcare organization. It manages personnel data, organizational structure, positions, and employee profiles for an Electronic Health Record (EHR) system. - -- **Type**: RESTful API backend with WebSocket support -- **Language**: TypeScript/Node.js -- **Database**: MySQL with TypeORM -- **API Framework**: TSOA (TypeScript OpenAPI) with auto-generated routes and Swagger docs - -## Common Commands - -### Development -```bash -npm run dev # Start development server with hot-reload (nodemon) -npm run build # Build for production (runs tsoa spec-and-routes && tsc) -npm start # Run production build (node ./dist/app.js) -npm run format # Format code with Prettier -npm run check # Type check without emitting (tsc --noEmit) -``` - -### Database Migrations - -**CRITICAL**: After generating any migration, you MUST run the cleanup script to remove FK/idx lines: - -```bash -# Generate new migration (include descriptive name) -npm run migration:generate src/migration/update_table_0811202s - -# CLEANUP: Remove FK_/idx_ lines from generated migration -node scripts/clean-migration-fk-idx.js - -# Run migrations -npm run migration:run -``` - -The cleanup script replaces lines containing `FK_` or `idx_` with `// removed FK_/idx_ auto-cleanup`. This is required because TypeORM generates foreign key and index constraints that must be manually removed. - -### Local GitHub Actions Testing -```bash -# Test release workflow locally using act -act workflow_dispatch -W .github/workflows/release.yaml \ - --input IMAGE_VER=latest \ - -s DOCKER_USER=admin \ - -s DOCKER_PASS=FPTadmin2357 \ - -s SSH_PASSWORD=FPTadmin2357 -``` - -## Architecture - -### Application Entry Point -`src/app.ts` - Initializes the application: -1. Database connection (TypeORM MySQL) -2. In-memory caches (LogMemoryStore, OrgStructureCache with 30min TTL) -3. WebSocket server for real-time updates -4. Express middleware and TSOA routes -5. Cronjobs (scheduled tasks) -6. RabbitMQ message queue consumer - -### Controllers (`src/controllers/`) -Handle HTTP requests. Main controllers include: -- `OrganizationController` - Organizational structure management -- `CommandController` - Workflow and command processing -- `ProfileSalaryController` - Salary and tenure management -- Controllers for positions, employees, auth, etc. - -Cronjob logic is embedded in controllers (not separate services). - -### Services (`src/services/`) -Business logic and external integrations: -- `OrganizationService.ts` - Core organizational logic -- `rabbitmq.ts` - RabbitMQ consumer for async processing -- `webSocket.ts` - Real-time updates to clients - -### Entities (`src/entities/`) -TypeORM database models for MySQL. Key entities: -- Organization hierarchy (OrgRoot, OrgChild1-4) -- Position management (Position, PosType, PosLevel, PosExecutive) -- Employee profiles and related data -- Command/Workflow entities - -### Middlewares (`src/middlewares/`) -- `auth.ts` - Keycloak Bearer token authentication -- `authWebService.ts` - API key authentication (`X-API-Key` header) -- `role.ts` - Role-based authorization -- `logs.ts` - Request logging -- `error.ts` - Global error handling -- `user.ts` - User context extraction - -### TSOA Configuration -`src/routes.ts` is auto-generated by TSOA from controller decorators. Regenerated by `npm run build`. - -- Swagger docs available at `/api-docs` -- Dual authentication: Keycloak bearer tokens and API keys -- API tags organized by domain (Organization, Position, Employee, etc.) - -## Scheduled Cronjobs - -All times in Bangkok timezone (UTC+7): - -| Schedule | Task | Controller | -|----------|------|------------| -| `0 0 3 * * *` | Daily revision processing | `OrganizationController.cronjobRevision()` | -| `0 0 2 * * *` | Daily command processing | `CommandController.cronjobCommand()` | -| `0 0 1 10 *` | Monthly retirement status update (10th of month) | `CommandController.cronjobUpdateRetirementStatus()` | -| `0 0 0 * * *` | Daily tenure updates | `ProfileSalaryController` (multiple tenure methods) | - -## External Dependencies - -- **Keycloak** - Authentication and authorization -- **RabbitMQ** - Message queue for async operations -- **WebSocket** (Socket.IO) - Real-time updates -- **Elasticsearch** - Logging -- **Redis** - Caching layer -- **TypeORM** - Database ORM - -## Code Style - -- **Prettier**: 2 spaces, 100 char width, trailing commas -- **TypeScript**: Strict mode enabled -- **Comments**: Mixed Thai and English -- **Date handling**: Custom `DateSerializer` for local timezone serialization - -## Important Notes - -1. **Migration cleanup is mandatory** - TypeORM generates FK and index constraints that break the migration system. Always run `node scripts/clean-migration-fk-idx.js` after `migration:generate`. - -2. **Organization caching** - `OrgStructureCache` provides in-memory caching of org structure (30 min TTL) for performance. - -3. **Graceful shutdown** - Application handles SIGTERM/SIGINT to close database connections, caches, and HTTP server properly. - -4. **Dual auth system** - Most endpoints use Keycloak bearer tokens, but some web service endpoints use `X-API-Key` header authentication. - -5. **Thai localization** - The system is primarily for Thai users; documentation and some content is in Thai, but code is in English. diff --git a/scripts/KEYCLOAK_SYNC_README.md b/scripts/KEYCLOAK_SYNC_README.md deleted file mode 100644 index 63d6deb1..00000000 --- a/scripts/KEYCLOAK_SYNC_README.md +++ /dev/null @@ -1,149 +0,0 @@ -# Keycloak Sync Scripts - -This directory contains standalone scripts for managing Keycloak users from the CLI. These scripts are useful for maintenance, setup, and troubleshooting Keycloak synchronization. - -## Prerequisites - -- Node.js and TypeScript installed -- Database connection configured in `.env` -- Keycloak connection configured in `.env` -- Run with `ts-node` or compile first - -## Environment Variables - -Ensure these are set in your `.env` file: - -```bash -# Database -DB_HOST=localhost -DB_PORT=3306 -DB_NAME=your_database -DB_USER=your_user -DB_PASSWORD=your_password - -# Keycloak -KC_URL=https://your-keycloak-url -KC_REALMS=your-realm -KC_SERVICE_ACCOUNT_CLIENT_ID=your-client-id -KC_SERVICE_ACCOUNT_SECRET=your-client-secret -``` - -## Scripts - -### 1. clear-orphaned-users.ts - -Deletes Keycloak users that don't exist in the database (Profile + ProfileEmployee tables). Useful for cleaning up invalid users. - -**Protected usernames** (never deleted): `super_admin`, `admin_issue` - -```bash -# Dry run (preview changes) -ts-node scripts/clear-orphaned-users.ts --dry-run - -# Live execution -ts-node scripts/clear-orphaned-users.ts -``` - -**Output:** -- Total users in Keycloak -- Total users in Database -- Orphaned users found -- Deleted/Skipped/Failed counts - -### 2. ensure-users.ts - -Checks and creates Keycloak users for all profiles. Includes role assignment (USER role) automatically. - -```bash -# Dry run (preview changes) -ts-node scripts/ensure-users.ts --dry-run - -# Live execution -ts-node scripts/ensure-users.ts - -# Test with limited profiles (for testing) -ts-node scripts/ensure-users.ts --dry-run --limit=10 -``` - -**Output:** -- Total profiles processed -- Created users (new Keycloak accounts) -- Verified users (already exists) -- Skipped profiles (no citizenId) -- Failed count - -### 3. sync-all.ts - -Syncs all attributes to Keycloak for users with existing Keycloak IDs. - -**Attributes synced:** -- `profileId` -- `orgRootDnaId` -- `orgChild1DnaId` -- `orgChild2DnaId` -- `orgChild3DnaId` -- `orgChild4DnaId` -- `empType` -- `prefix` - -```bash -# Dry run (preview changes) -ts-node scripts/sync-all.ts --dry-run - -# Live execution -ts-node scripts/sync-all.ts - -# Test with limited profiles -ts-node scripts/sync-all.ts --dry-run --limit=10 - -# Adjust concurrency (default: 5) -ts-node scripts/sync-all.ts --concurrency=10 -``` - -**Output:** -- Total profiles with Keycloak IDs -- Success/Failed counts -- Error details for failures - -## Recommended Execution Order - -For initial setup or full resynchronization: - -1. **clear-orphaned-users** - Clean up invalid users first - ```bash - npx ts-node scripts/clear-orphaned-users.ts - ``` - -2. **ensure-users** - Create missing users and assign roles - ```bash - npx ts-node scripts/ensure-users.ts - ``` - -3. **sync-all** - Sync all attributes to Keycloak - ```bash - npx ts-node scripts/sync-all.ts - ``` - -## Tips - -- Always run with `--dry-run` first to preview changes -- Use `--limit=N` for testing before running on full dataset -- Scripts process both Profile (ข้าราชการ) and ProfileEmployee (ลูกจ้าง) tables -- Only active profiles (`isLeave: false`) are processed by ensure-users -- The `USER` role is automatically assigned to new/verified users - -## Troubleshooting - -**Database connection error:** -- Check `.env` database variables -- Ensure database server is running - -**Keycloak connection error:** -- Check `KC_URL`, `KC_REALMS` in `.env` -- Verify service account credentials -- Check network connectivity to Keycloak - -**USER role not found:** -- Log in to Keycloak admin console -- Create a `USER` role in your realm -- Ensure service account has `manage-users` and `view-users` permissions diff --git a/scripts/assign-user-role.ts b/scripts/assign-user-role.ts deleted file mode 100644 index 3a1ca878..00000000 --- a/scripts/assign-user-role.ts +++ /dev/null @@ -1,269 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { Profile } from "../src/entities/Profile"; -import { ProfileEmployee } from "../src/entities/ProfileEmployee"; -import * as keycloak from "../src/keycloak/index"; - -const USER_ROLE_NAME = "USER"; - -interface AssignOptions { - dryRun: boolean; - targetUsernames?: string[]; -} - -interface UserWithKeycloak { - keycloakId: string; - citizenId: string; - source: "Profile" | "ProfileEmployee"; -} - -interface AssignResult { - total: number; - assigned: number; - skipped: number; - failed: number; - errors: Array<{ - userId: string; - username: string; - error: string; - }>; -} - -/** - * Get all users from database who have Keycloak IDs set - */ -async function getUsersWithKeycloak(): Promise { - const users: UserWithKeycloak[] = []; - const profileRepo = AppDataSource.getRepository(Profile); - const profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); - - // Get from Profile table - const profiles = await profileRepo - .createQueryBuilder("profile") - .where("profile.keycloak IS NOT NULL") - .andWhere("profile.keycloak != ''") - .getMany(); - - for (const profile of profiles) { - users.push({ - keycloakId: profile.keycloak, - citizenId: profile.citizenId || profile.id, - source: "Profile", - }); - } - - // Get from ProfileEmployee table - const employees = await profileEmployeeRepo - .createQueryBuilder("profileEmployee") - .where("profileEmployee.keycloak IS NOT NULL") - .andWhere("profileEmployee.keycloak != ''") - .getMany(); - - for (const employee of employees) { - // Avoid duplicates - check if keycloak ID already exists - if (!users.some((u) => u.keycloakId === employee.keycloak)) { - users.push({ - keycloakId: employee.keycloak, - citizenId: employee.citizenId || employee.id, - source: "ProfileEmployee", - }); - } - } - - return users; -} - -/** - * Assign USER role to users who don't have it - */ -async function assignUserRoleToUsers( - users: UserWithKeycloak[], - userRoleId: string, - options: AssignOptions, -): Promise { - const result: AssignResult = { - total: users.length, - assigned: 0, - skipped: 0, - failed: 0, - errors: [], - }; - - console.log(`Processing ${result.total} users...`); - - for (let i = 0; i < users.length; i++) { - const user = users[i]; - const index = i + 1; - - try { - // Get user's current roles - const userRoles = await keycloak.getUserRoles(user.keycloakId); - - if (!userRoles || typeof userRoles === "boolean") { - console.log( - `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Failed to get roles`, - ); - result.failed++; - result.errors.push({ - userId: user.keycloakId, - username: user.citizenId, - error: "Failed to get user roles", - }); - continue; - } - - // Handle both array and single object return types - // getUserRoles can return an array or a single object - const rolesArray = Array.isArray(userRoles) ? userRoles : [userRoles]; - - // Check if user already has USER role - const hasUserRole = rolesArray.some((role: { id: string; name: string }) => - role.name === USER_ROLE_NAME, - ); - - if (hasUserRole) { - console.log( - `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Already has USER role`, - ); - result.skipped++; - continue; - } - - // Assign USER role - if (options.dryRun) { - console.log( - `[${index}/${result.total}] [DRY-RUN] Would assign USER role: ${user.citizenId} (source: ${user.source})`, - ); - result.assigned++; - } else { - const assignResult = await keycloak.addUserRoles(user.keycloakId, [ - { id: userRoleId, name: USER_ROLE_NAME }, - ]); - - if (assignResult) { - console.log( - `[${index}/${result.total}] Assigned USER role: ${user.citizenId} (source: ${user.source})`, - ); - result.assigned++; - } else { - console.log( - `[${index}/${result.total}] Failed: ${user.citizenId} (source: ${user.source}) - Could not assign role`, - ); - result.failed++; - result.errors.push({ - userId: user.keycloakId, - username: user.citizenId, - error: "Failed to assign USER role", - }); - } - } - - // Small delay to avoid rate limiting - if (index % 50 === 0) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.log( - `[${index}/${result.total}] Error: ${user.citizenId} (source: ${user.source}) - ${errorMessage}`, - ); - result.failed++; - result.errors.push({ - userId: user.keycloakId, - username: user.citizenId, - error: errorMessage, - }); - } - } - - return result; -} - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - - console.log("=".repeat(60)); - console.log("Keycloak USER Role Assignment Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - // Get USER role from Keycloak - console.log(""); - const userRole = await keycloak.getRoles(USER_ROLE_NAME); - - // Check if USER role exists and is valid (has id property) - if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { - console.error(`ERROR: ${USER_ROLE_NAME} role not found in Keycloak`); - await AppDataSource.destroy(); - process.exit(1); - } - - // Type assertion via unknown to bypass union type issues - const role = userRole as unknown as { id: string; name: string; description?: string }; - const roleId = role.id; - console.log(`${USER_ROLE_NAME} role found: ${roleId}`); - console.log(""); - - // Get users from database - console.log("Fetching users from database..."); - let users = await getUsersWithKeycloak(); - - if (users.length === 0) { - console.log("No users with Keycloak IDs found in database"); - await AppDataSource.destroy(); - process.exit(0); - } - - console.log(`Found ${users.length} users with Keycloak IDs`); - console.log(""); - - // Assign USER role - const result = await assignUserRoleToUsers(users, roleId, { dryRun }); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total users: ${result.total}`); - console.log(` Assigned: ${result.assigned}`); - console.log(` Skipped: ${result.skipped}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.errors.length > 0) { - console.log(""); - console.log("Errors:"); - result.errors.forEach((e) => { - console.log(` ${e.username}: ${e.error}`); - }); - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/scripts/clear-orphaned-users.ts b/scripts/clear-orphaned-users.ts deleted file mode 100644 index 67eee99d..00000000 --- a/scripts/clear-orphaned-users.ts +++ /dev/null @@ -1,80 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; -import * as keycloak from "../src/keycloak/index"; - -const PROTECTED_USERNAMES = ["super_admin", "admin_issue"]; - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - const skipUsernames = [...PROTECTED_USERNAMES]; - - console.log("=".repeat(60)); - console.log("Clear Orphaned Keycloak Users Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - console.log(`Protected usernames: ${skipUsernames.join(", ")}`); - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log(""); - - // Run the orphaned user cleanup - const service = new KeycloakAttributeService(); - const result = await service.clearOrphanedKeycloakUsers(skipUsernames); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total users in Keycloak: ${result.totalInKeycloak}`); - console.log(` Total users in Database: ${result.totalInDatabase}`); - console.log(` Orphaned users: ${result.orphanedCount}`); - console.log(` Deleted: ${result.deleted}`); - console.log(` Skipped: ${result.skipped}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.details.length > 0) { - console.log(""); - console.log("Details:"); - for (const detail of result.details) { - const status = - detail.action === "deleted" - ? "[DELETED]" - : detail.action === "skipped" - ? "[SKIPPED]" - : "[ERROR]"; - console.log( - ` ${status} ${detail.username} (${detail.keycloakUserId})${detail.error ? ": " + detail.error : ""}`, - ); - } - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/scripts/ensure-users.ts b/scripts/ensure-users.ts deleted file mode 100644 index 14522d96..00000000 --- a/scripts/ensure-users.ts +++ /dev/null @@ -1,91 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; -import * as keycloak from "../src/keycloak/index"; - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - const limitArg = args.find((arg) => arg.startsWith("--limit=")); - const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; - - console.log("=".repeat(60)); - console.log("Ensure Keycloak Users Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - if (limit !== undefined) { - console.log(`Limit: ${limit} profiles per table (for testing)`); - } - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log(""); - - // Verify USER role exists - console.log("Verifying USER role in Keycloak..."); - const userRole = await keycloak.getRoles("USER"); - - if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { - console.error("ERROR: USER role not found in Keycloak"); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log("USER role found"); - console.log(""); - - // Run the ensure users operation - const service = new KeycloakAttributeService(); - console.log("Ensuring Keycloak users for all profiles..."); - console.log(""); - - const result = await service.batchEnsureKeycloakUsers(); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total profiles: ${result.total}`); - console.log(` Created: ${result.created}`); - console.log(` Verified: ${result.verified}`); - console.log(` Skipped: ${result.skipped}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.failed > 0) { - console.log(""); - console.log("Failed Details:"); - const failedDetails = result.details.filter((d) => d.action === "error" || !!d.error); - for (const detail of failedDetails) { - console.log( - ` [${detail.profileType}] ${detail.profileId}: ${detail.error || "Unknown error"}`, - ); - } - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/scripts/sync-all.ts b/scripts/sync-all.ts deleted file mode 100644 index 9090dd17..00000000 --- a/scripts/sync-all.ts +++ /dev/null @@ -1,93 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; -import * as keycloak from "../src/keycloak/index"; - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - const limitArg = args.find((arg) => arg.startsWith("--limit=")); - const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; - const concurrencyArg = args.find((arg) => arg.startsWith("--concurrency=")); - const concurrency = concurrencyArg ? parseInt(concurrencyArg.split("=")[1], 10) : undefined; - - console.log("=".repeat(60)); - console.log("Sync All Attributes to Keycloak Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - if (limit !== undefined) { - console.log(`Limit: ${limit} profiles per table (for testing)`); - } - if (concurrency !== undefined) { - console.log(`Concurrency: ${concurrency}`); - } - console.log(""); - - console.log("Attributes to sync:"); - console.log(" - profileId"); - console.log(" - orgRootDnaId"); - console.log(" - orgChild1DnaId"); - console.log(" - orgChild2DnaId"); - console.log(" - orgChild3DnaId"); - console.log(" - orgChild4DnaId"); - console.log(" - empType"); - console.log(" - prefix"); - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log(""); - - // Run the sync operation - const service = new KeycloakAttributeService(); - console.log("Syncing attributes for all profiles with Keycloak IDs..."); - console.log(""); - - const result = await service.batchSyncUsers({ limit, concurrency }); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total profiles: ${result.total}`); - console.log(` Success: ${result.success}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.failed > 0) { - console.log(""); - console.log("Failed Details:"); - for (const detail of result.details.filter( - (d) => d.status === "failed" || d.status === "error", - )) { - console.log( - ` ${detail.profileId} (${detail.keycloakUserId}): ${detail.error || "Sync failed"}`, - ); - } - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/src/app.ts b/src/app.ts index 06f76548..75d0bfea 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,7 +15,6 @@ import { logMemoryStore } from "./utils/LogMemoryStore"; import { orgStructureCache } from "./utils/OrgStructureCache"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; -import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; @@ -53,19 +52,8 @@ async function main() { const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_PORT = +(process.env.APP_PORT || 3000); - // Cron job for executing command - every day at 00:30:00 - const cronTime_command = "0 30 0 * * *"; - cron.schedule(cronTime_command, async () => { - try { - const commandController = new CommandController(); - await commandController.cronjobCommand(); - } catch (error) { - console.error("Error executing function from controller:", error); - } - }); - - // Cron job for updating org revision - every day at 01:00:00 - const cronTime = "0 0 1 * * *"; + const cronTime = "0 0 3 * * *"; // ตั้งเวลาทุกวันเวลา 03:00:00 + // const cronTime = "*/10 * * * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); @@ -75,8 +63,18 @@ async function main() { } }); - // Cron job for updating retirement status - every day at 02:00:00 on the 1st of October - const cronTime_Oct = "0 0 2 10 *"; + const cronTime_command = "0 0 2 * * *"; + // const cronTime_command = "*/10 * * * * *"; + cron.schedule(cronTime_command, async () => { + try { + const commandController = new CommandController(); + await commandController.cronjobCommand(); + } catch (error) { + console.error("Error executing function from controller:", error); + } + }); + + const cronTime_Oct = "0 0 1 10 *"; cron.schedule(cronTime_Oct, async () => { try { const commandController = new CommandController(); @@ -86,19 +84,7 @@ async function main() { } }); - // Cron job for updating org DNA - every day at 03:00:00 - const cronTime_UpdateOrg = "0 0 3 * * *"; - cron.schedule(cronTime_UpdateOrg, async () => { - try { - const scriptProfileOrgController = new ScriptProfileOrgController(); - await scriptProfileOrgController.cronjobUpdateOrg({} as any); - } catch (error) { - console.error("Error executing cronjobUpdateOrg:", error); - } - }); - - // Cron job for updating tenure - every day at 04:00:00 - const cronTime_Tenure = "0 0 4 * * *"; + const cronTime_Tenure = "0 0 0 * * *"; cron.schedule(cronTime_Tenure, async () => { try { const profileSalaryController = new ProfileSalaryController(); diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts deleted file mode 100644 index 5f814238..00000000 --- a/src/controllers/KeycloakSyncController.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - Controller, - Post, - Get, - Route, - Security, - Tags, - Path, - Request, - Response, - Query, - Body, -} from "tsoa"; -import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; -import HttpSuccess from "../interfaces/http-success"; -import HttpStatus from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { RequestWithUser } from "../middlewares/user"; - -@Route("api/v1/org/keycloak-sync") -@Tags("Keycloak Sync") -@Security("bearerAuth") -@Response( - HttpStatus.INTERNAL_SERVER_ERROR, - "เกิดข้อผิดพลาด ไม่สามารถดำเนินการได้ กรุณาลองใหม่ในภายหลัง", -) -export class KeycloakSyncController extends Controller { - private keycloakAttributeService = new KeycloakAttributeService(); - - /** - * Sync attributes for the current logged-in user - * - * @summary Sync profileId and rootDnaId to Keycloak for current user - */ - @Post("sync-me") - async syncCurrentUser(@Request() request: RequestWithUser) { - const keycloakUserId = request.user.sub; - - if (!keycloakUserId) { - throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); - } - - // Get attributes from database before sync - const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); - - const success = await this.keycloakAttributeService.syncUserAttributes(keycloakUserId); - - if (!success) { - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ กรุณาติดต่อผู้ดูแลระบบ", - ); - } - - // Verify sync by fetching attributes from Keycloak after update - const kcAttrsAfter = - await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); - - return new HttpSuccess({ - message: "Sync ข้อมูลสำเร็จ", - syncedToKeycloak: !!kcAttrsAfter?.profileId, - databaseAttributes: dbAttrs, - keycloakAttributesAfter: kcAttrsAfter, - }); - } - - /** - * Get current attributes of the logged-in user - * - * @summary Get current profileId and rootDnaId from Keycloak - */ - // @Get("my-attributes") - async getMyAttributes(@Request() request: RequestWithUser) { - const keycloakUserId = request.user.sub; - - if (!keycloakUserId) { - throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); - } - - const keycloakAttributes = - await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); - const dbAttributes = - await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); - - return new HttpSuccess({ - keycloakAttributes, - databaseAttributes: dbAttributes, - }); - } - - /** - * Sync attributes for a specific profile (Admin only) - * - * @summary Sync profileId and rootDnaId to Keycloak by profile ID (ADMIN) - * - * @param {string} profileId Profile ID - * @param {string} profileType Profile type (PROFILE or PROFILE_EMPLOYEE) - */ - @Post("sync-profile/:profileId") - async syncByProfileId( - @Path() profileId: string, - @Query() profileType: "PROFILE" | "PROFILE_EMPLOYEE" = "PROFILE", - ) { - if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { - throw new HttpError( - HttpStatus.BAD_REQUEST, - "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", - ); - } - - const success = await this.keycloakAttributeService.syncOnOrganizationChange( - profileId, - profileType, - ); - - if (!success) { - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ หรือไม่พบข้อมูล profile", - ); - } - - return new HttpSuccess({ message: "Sync ข้อมูลสำเร็จ" }); - } - - /** - * Batch sync attributes for multiple profiles (Admin only) - * - * @summary Batch sync profileId and rootDnaId to Keycloak for multiple profiles (ADMIN) - * - * @param {request} request Request body containing profileIds array and profileType - */ - // @Post("sync-profiles-batch") - async syncByProfileIds( - @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" }, - ) { - const { profileIds, profileType } = request; - - // Validate profileIds - if (!profileIds || profileIds.length === 0) { - throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า"); - } - - // Validate profileType - if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { - throw new HttpError( - HttpStatus.BAD_REQUEST, - "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", - ); - } - - const result = { - total: profileIds.length, - success: 0, - failed: 0, - details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>, - }; - - // Process each profileId - for (const profileId of profileIds) { - try { - const success = await this.keycloakAttributeService.syncOnOrganizationChange( - profileId, - profileType, - ); - - if (success) { - result.success++; - result.details.push({ profileId, status: "success" }); - } else { - result.failed++; - result.details.push({ - profileId, - status: "failed", - error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID", - }); - } - } catch (error: any) { - result.failed++; - result.details.push({ profileId, status: "failed", error: error.message }); - } - } - - return new HttpSuccess({ - message: "Batch sync เสร็จสิ้น", - ...result, - }); - } - - /** - * Batch sync all users (Admin only) - * - * @summary Batch sync all users to Keycloak without limit (ADMIN) - * - * @description Syncs profileId and orgRootDnaId to Keycloak for all users - * that have a keycloak ID. Uses parallel processing for better performance. - */ - // @Post("sync-all") - async syncAll() { - const result = await this.keycloakAttributeService.batchSyncUsers(); - - return new HttpSuccess({ - message: "Batch sync เสร็จสิ้น", - total: result.total, - success: result.success, - failed: result.failed, - details: result.details, - }); - } - - /** - * Ensure Keycloak users exist for all profiles (Admin only) - * - * @summary Create or verify Keycloak users for all profiles in Profile and ProfileEmployee tables (ADMIN) - * - * @description - * This endpoint will: - * - Create new Keycloak users for profiles without a keycloak ID - * - Create new Keycloak users for profiles where the stored keycloak ID doesn't exist in Keycloak - * - Verify existing Keycloak users - * - Skip profiles without a citizenId - */ - // @Post("ensure-users") - async ensureAllUsers() { - const result = await this.keycloakAttributeService.batchEnsureKeycloakUsers(); - return new HttpSuccess({ - message: "Batch ensure Keycloak users เสร็จสิ้น", - ...result, - }); - } - - /** - * Clear orphaned Keycloak users (Admin only) - * - * @summary Delete Keycloak users that are not in the database (ADMIN) - * - * @description - * This endpoint will: - * - Find users in Keycloak that are not referenced in Profile or ProfileEmployee tables - * - Delete those orphaned users from Keycloak - * - Skip protected users (super_admin, admin_issue) - * - * @param {request} request Request body containing skipUsernames array - */ - // @Post("clear-orphaned-users") - async clearOrphanedUsers(@Body() request?: { skipUsernames?: string[] }) { - const skipUsernames = request?.skipUsernames || ["super_admin", "admin_issue"]; - const result = await this.keycloakAttributeService.clearOrphanedKeycloakUsers(skipUsernames); - return new HttpSuccess({ - message: "Clear orphaned Keycloak users เสร็จสิ้น", - ...result, - }); - } -} diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts deleted file mode 100644 index c8975e43..00000000 --- a/src/controllers/ScriptProfileOrgController.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { Controller, Post, Route, Security, Tags, Request } from "tsoa"; -import { AppDataSource } from "../database/data-source"; -import HttpSuccess from "../interfaces/http-success"; -import HttpStatus from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { RequestWithUser } from "../middlewares/user"; -import { MoreThanOrEqual } from "typeorm"; -import { PosMaster } from "./../entities/PosMaster"; -import axios from "axios"; -import { KeycloakSyncController } from "./KeycloakSyncController"; -import { EmployeePosMaster } from "./../entities/EmployeePosMaster"; - -interface OrgUpdatePayload { - profileId: string; - rootDnaId: string | null; - child1DnaId: string | null; - child2DnaId: string | null; - child3DnaId: string | null; - child4DnaId: string | null; - profileType: "PROFILE" | "PROFILE_EMPLOYEE"; -} - -@Route("api/v1/org/script-profile-org") -@Tags("Keycloak Sync") -@Security("bearerAuth") -export class ScriptProfileOrgController extends Controller { - private posMasterRepo = AppDataSource.getRepository(PosMaster); - private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); - - // Idempotency flag to prevent concurrent runs - private isRunning = false; - - // Configurable values - private readonly BATCH_SIZE = parseInt(process.env.CRONJOB_BATCH_SIZE || "100", 10); - private readonly UPDATE_WINDOW_HOURS = parseInt( - process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24", - 10, - ); - - @Post("update-org") - public async cronjobUpdateOrg(@Request() request: RequestWithUser) { - // Idempotency check - prevent concurrent runs - if (this.isRunning) { - console.log("cronjobUpdateOrg: Job already running, skipping this execution"); - return new HttpSuccess({ - message: "Job already running", - skipped: true, - }); - } - - this.isRunning = true; - const startTime = Date.now(); - - try { - const windowStart = new Date(Date.now() - this.UPDATE_WINDOW_HOURS * 60 * 60 * 1000); - - console.log("cronjobUpdateOrg: Starting job", { - windowHours: this.UPDATE_WINDOW_HOURS, - windowStart: windowStart.toISOString(), - batchSize: this.BATCH_SIZE, - }); - - // Query with optimized select - only fetch required fields - const [posMasters, posMasterEmployee] = await Promise.all([ - this.posMasterRepo.find({ - where: { - lastUpdatedAt: MoreThanOrEqual(windowStart), - orgRevision: { - orgRevisionIsCurrent: true, - }, - }, - relations: [ - "orgRevision", - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - ], - select: { - id: true, - current_holderId: true, - lastUpdatedAt: true, - orgRevision: { id: true }, - orgRoot: { ancestorDNA: true }, - orgChild1: { ancestorDNA: true }, - orgChild2: { ancestorDNA: true }, - orgChild3: { ancestorDNA: true }, - orgChild4: { ancestorDNA: true }, - current_holder: { id: true }, - }, - }), - this.employeePosMasterRepo.find({ - where: { - lastUpdatedAt: MoreThanOrEqual(windowStart), - orgRevision: { - orgRevisionIsCurrent: true, - }, - }, - relations: [ - "orgRevision", - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - ], - select: { - id: true, - current_holderId: true, - lastUpdatedAt: true, - orgRevision: { id: true }, - orgRoot: { ancestorDNA: true }, - orgChild1: { ancestorDNA: true }, - orgChild2: { ancestorDNA: true }, - orgChild3: { ancestorDNA: true }, - orgChild4: { ancestorDNA: true }, - current_holder: { id: true }, - }, - }), - ]); - - console.log("cronjobUpdateOrg: Database query completed", { - posMastersCount: posMasters.length, - employeePosCount: posMasterEmployee.length, - totalRecords: posMasters.length + posMasterEmployee.length, - }); - - // Build payloads with proper profile type tracking - const payloads = this.buildPayloads(posMasters, posMasterEmployee); - - if (payloads.length === 0) { - console.log("cronjobUpdateOrg: No records to process"); - return new HttpSuccess({ - message: "No records to process", - processed: 0, - }); - } - - // Update profile's org structure in leave service by calling API - console.log("cronjobUpdateOrg: Calling leave service API", { - payloadCount: payloads.length, - }); - - await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, { - headers: { - "Content-Type": "application/json", - api_key: process.env.API_KEY, - }, - timeout: 30000, // 30 second timeout - }); - - console.log("cronjobUpdateOrg: Leave service API call successful"); - - // Group profile IDs by type for proper syncing - const profileIdsByType = this.groupProfileIdsByType(payloads); - - // Sync to Keycloak with batching - const keycloakSyncController = new KeycloakSyncController(); - const syncResults = { - total: 0, - success: 0, - failed: 0, - byType: {} as Record, - }; - - // Process each profile type separately - for (const [profileType, profileIds] of Object.entries(profileIdsByType)) { - console.log(`cronjobUpdateOrg: Syncing ${profileType} profiles`, { - count: profileIds.length, - }); - - const batches = this.chunkArray(profileIds, this.BATCH_SIZE); - const typeResult = { total: profileIds.length, success: 0, failed: 0 }; - - for (let i = 0; i < batches.length; i++) { - const batch = batches[i]; - console.log( - `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`, - { - batchSize: batch.length, - batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min( - (i + 1) * this.BATCH_SIZE, - profileIds.length, - )}`, - }, - ); - - try { - const batchResult: any = await keycloakSyncController.syncByProfileIds({ - profileIds: batch, - profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", - }); - - // Extract result data if available - const resultData = (batchResult as any)?.data || batchResult; - typeResult.success += resultData.success || 0; - typeResult.failed += resultData.failed || 0; - - console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { - success: resultData.success || 0, - failed: resultData.failed || 0, - }); - } catch (error: any) { - console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { - error: error.message, - batchSize: batch.length, - }); - // Count all profiles in failed batch as failed - typeResult.failed += batch.length; - } - } - - syncResults.byType[profileType] = typeResult; - syncResults.total += typeResult.total; - syncResults.success += typeResult.success; - syncResults.failed += typeResult.failed; - } - - const duration = Date.now() - startTime; - console.log("cronjobUpdateOrg: Job completed", { - duration: `${duration}ms`, - processed: payloads.length, - syncResults, - }); - - return new HttpSuccess({ - message: "Update org completed", - processed: payloads.length, - syncResults, - duration: `${duration}ms`, - }); - } catch (error: any) { - const duration = Date.now() - startTime; - console.error("cronjobUpdateOrg: Job failed", { - duration: `${duration}ms`, - error: error.message, - stack: error.stack, - }); - throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"); - } finally { - this.isRunning = false; - } - } - - /** - * Build payloads from PosMaster and EmployeePosMaster records - * Includes proper profile type tracking for accurate Keycloak sync - */ - private buildPayloads( - posMasters: PosMaster[], - posMasterEmployee: EmployeePosMaster[], - ): OrgUpdatePayload[] { - const payloads: OrgUpdatePayload[] = []; - - // Process PosMaster records (PROFILE type) - for (const posMaster of posMasters) { - if (posMaster.current_holder && posMaster.current_holderId) { - payloads.push({ - profileId: posMaster.current_holderId, - rootDnaId: posMaster.orgRoot?.ancestorDNA || null, - child1DnaId: posMaster.orgChild1?.ancestorDNA || null, - child2DnaId: posMaster.orgChild2?.ancestorDNA || null, - child3DnaId: posMaster.orgChild3?.ancestorDNA || null, - child4DnaId: posMaster.orgChild4?.ancestorDNA || null, - profileType: "PROFILE", - }); - } - } - - // Process EmployeePosMaster records (PROFILE_EMPLOYEE type) - for (const employeePos of posMasterEmployee) { - if (employeePos.current_holder && employeePos.current_holderId) { - payloads.push({ - profileId: employeePos.current_holderId, - rootDnaId: employeePos.orgRoot?.ancestorDNA || null, - child1DnaId: employeePos.orgChild1?.ancestorDNA || null, - child2DnaId: employeePos.orgChild2?.ancestorDNA || null, - child3DnaId: employeePos.orgChild3?.ancestorDNA || null, - child4DnaId: employeePos.orgChild4?.ancestorDNA || null, - profileType: "PROFILE_EMPLOYEE", - }); - } - } - - return payloads; - } - - /** - * Group profile IDs by their type for separate Keycloak sync calls - */ - private groupProfileIdsByType(payloads: OrgUpdatePayload[]): Record { - const grouped: Record = { - PROFILE: [], - PROFILE_EMPLOYEE: [], - }; - - for (const payload of payloads) { - grouped[payload.profileType].push(payload.profileId); - } - - // Remove empty groups and deduplicate IDs within each group - const result: Record = {}; - for (const [type, ids] of Object.entries(grouped)) { - if (ids.length > 0) { - // Deduplicate while preserving order - result[type] = Array.from(new Set(ids)); - } - } - - return result; - } - - /** - * Split array into chunks of specified size - */ - private chunkArray(array: T[], chunkSize: number): T[][] { - const chunks: T[][] = []; - for (let i = 0; i < array.length; i += chunkSize) { - chunks.push(array.slice(i, i + chunkSize)); - } - return chunks; - } -} diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index 835e31f2..a81e2af9 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -4,8 +4,8 @@ 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; const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET; -// const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; -// const API_KEY = process.env.API_KEY; +const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; +const API_KEY = process.env.API_KEY; let token: string | null = null; let decoded: DecodedJwt | null = null; @@ -165,119 +165,16 @@ export async function getUserList(first = "", max = "", search = "") { if (!res) return false; if (!res.ok) { - const errorText = await res.text(); - return Boolean(console.error("Keycloak Error Response: ", errorText)); + return Boolean(console.error("Keycloak Error Response: ", await res.json())); } - // Get raw text first to handle potential JSON parsing errors - const rawText = await res.text(); - - // Log response size for debugging - console.log(`[getUserList] Response size: ${rawText.length} bytes`); - - try { - const data = JSON.parse(rawText) as any[]; - return data.map((v: Record) => ({ - id: v.id, - username: v.username, - firstName: v.firstName, - lastName: v.lastName, - email: v.email, - enabled: v.enabled, - })); - } catch (error) { - console.error(`[getUserList] Failed to parse JSON response:`); - console.error(`[getUserList] Response preview (first 500 chars):`, rawText.substring(0, 500)); - console.error(`[getUserList] Response preview (last 200 chars):`, rawText.slice(-200)); - throw new Error( - `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, - ); - } -} - -/** - * Get all keycloak users with pagination to avoid response size limits - * - * Client must have permission to manage realm's user - * - * @returns user list if success, false otherwise. - */ -export async function getAllUsersPaginated( - search: string = "", - batchSize: number = 100, -): Promise< - | Array<{ - id: string; - username: string; - firstName?: string; - lastName?: string; - email?: string; - enabled: boolean; - }> - | false -> { - const allUsers: any[] = []; - let first = 0; - let hasMore = true; - - while (hasMore) { - const res = await fetch( - `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first}&max=${batchSize}${search ? `&search=${search}` : ""}`, - { - headers: { - authorization: `Bearer ${await getToken()}`, - "content-type": `application/json`, - }, - }, - ).catch((e) => console.log("Keycloak Error: ", e)); - - if (!res) return false; - if (!res.ok) { - const errorText = await res.text(); - console.error("Keycloak Error Response: ", errorText); - return false; - } - - const rawText = await res.text(); - - try { - const batch = JSON.parse(rawText) as any[]; - - if (batch.length === 0) { - hasMore = false; - } else { - allUsers.push(...batch); - first += batch.length; - hasMore = batch.length === batchSize; - - // Log progress for large datasets - if (allUsers.length % 500 === 0) { - console.log(`[getAllUsersPaginated] Fetched ${allUsers.length} users so far...`); - } - } - } catch (error) { - console.error(`[getAllUsersPaginated] Failed to parse JSON response at offset ${first}:`); - console.error( - `[getAllUsersPaginated] Response preview (first 500 chars):`, - rawText.substring(0, 500), - ); - console.error( - `[getAllUsersPaginated] Response preview (last 200 chars):`, - rawText.slice(-200), - ); - throw new Error(`Failed to parse Keycloak response as JSON at batch starting at ${first}.`); - } - } - - console.log(`[getAllUsersPaginated] Total users fetched: ${allUsers.length}`); - - return allUsers.map((v: any) => ({ + return ((await res.json()) as any[]).map((v: Record) => ({ id: v.id, username: v.username, firstName: v.firstName, lastName: v.lastName, email: v.email, - enabled: v.enabled === true || v.enabled === "true", + enabled: v.enabled, })); } @@ -323,34 +220,17 @@ export async function getUserListOrg(first = "", max = "", search = "", userIds: if (!res) return false; if (!res.ok) { - const errorText = await res.text(); - return Boolean(console.error("Keycloak Error Response: ", errorText)); + return Boolean(console.error("Keycloak Error Response: ", await res.json())); } - // Get raw text first to handle potential JSON parsing errors - const rawText = await res.text(); - - try { - const data = JSON.parse(rawText) as any[]; - return data.map((v: Record) => ({ - id: v.id, - username: v.username, - firstName: v.firstName, - lastName: v.lastName, - email: v.email, - enabled: v.enabled, - })); - } catch (error) { - console.error(`[getUserListOrg] Failed to parse JSON response:`); - console.error( - `[getUserListOrg] Response preview (first 500 chars):`, - rawText.substring(0, 500), - ); - console.error(`[getUserListOrg] Response preview (last 200 chars):`, rawText.slice(-200)); - throw new Error( - `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, - ); - } + return ((await res.json()) as any[]).map((v: Record) => ({ + id: v.id, + username: v.username, + firstName: v.firstName, + lastName: v.lastName, + email: v.email, + enabled: v.enabled, + })); } export async function getUserCountOrg(first = "", max = "", search = "", userIds: string[] = []) { @@ -564,12 +444,10 @@ export async function getRoles(name?: string, token?: string) { })); } - // Return single role object - return { - id: data.id, - name: data.name, - description: data.description, - }; + // return { + // id: data.id, + // name: data.name, + // }; } /** @@ -894,73 +772,6 @@ export async function changeUserPassword(userId: string, newPassword: string) { } } -/** - * Update user attributes in Keycloak - * - * @param userId - Keycloak user ID - * @param attributes - Object containing attribute names and their values (as arrays) - * @returns true if success, false otherwise - */ -export async function updateUserAttributes( - userId: string, - attributes: Record, -): Promise { - try { - // Get existing user data to preserve other attributes - const existingUser = await getUser(userId); - - if (!existingUser) { - console.error(`User ${userId} not found in Keycloak`); - return false; - } - - // Merge existing attributes with new attributes - // IMPORTANT: Spread all existing user fields to preserve firstName, lastName, email, etc. - // The Keycloak PUT endpoint performs a full update, so we must include all fields - const updatedAttributes = { - ...existingUser, - attributes: { - ...(existingUser.attributes || {}), - ...attributes, - }, - }; - - console.log( - `[updateUserAttributes] Sending to Keycloak:`, - JSON.stringify(updatedAttributes, null, 2), - ); - - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { - headers: { - authorization: `Bearer ${await getToken()}`, - "content-type": "application/json", - }, - method: "PUT", - body: JSON.stringify(updatedAttributes), - }).catch((e) => { - console.error(`[updateUserAttributes] Network error:`, e); - return null; - }); - - if (!res) { - console.error(`[updateUserAttributes] No response from Keycloak`); - return false; - } - - if (!res.ok) { - const errorText = await res.text(); - console.error(`[updateUserAttributes] Keycloak Error (${res.status}):`, errorText); - return false; - } - - console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`); - return true; - } catch (error) { - console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error); - return false; - } -} - // Function to reset password export async function resetPassword(username: string) { try { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index 9a571572..c27e6188 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -75,16 +75,6 @@ export async function expressAuthentication( request.app.locals.logData.userName = payload.name; request.app.locals.logData.user = payload.preferred_username; - // เก็บค่า profileId และ orgRootDnaId จาก token (ใช้ค่าว่างถ้าไม่มี) - request.app.locals.logData.profileId = payload.profileId ?? ""; - request.app.locals.logData.orgRootDnaId = payload.orgRootDnaId ?? ""; - request.app.locals.logData.orgChild1DnaId = payload.orgChild1DnaId ?? ""; - request.app.locals.logData.orgChild2DnaId = payload.orgChild2DnaId ?? ""; - request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? ""; - request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? ""; - request.app.locals.logData.empType = payload.empType ?? ""; - request.app.locals.logData.prefix = payload.prefix ?? ""; - return payload; } diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index 225f0a37..e5c48d9a 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -9,15 +9,6 @@ export type RequestWithUser = Request & { preferred_username: string; email: string; role: string[]; - profileId?: string; - orgRootDnaId?: string; - orgChild1DnaId?: string; - orgChild2DnaId?: string; - orgChild3DnaId?: string; - orgChild4DnaId?: string; - empType?: string; - prefix?: string; - scope?: string; }; }; diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts deleted file mode 100644 index 5b61e286..00000000 --- a/src/services/KeycloakAttributeService.ts +++ /dev/null @@ -1,928 +0,0 @@ -import { AppDataSource } from "../database/data-source"; -import { Profile } from "../entities/Profile"; -import { ProfileEmployee } from "../entities/ProfileEmployee"; -// import { PosMaster } from "../entities/PosMaster"; -// import { EmployeePosMaster } from "../entities/EmployeePosMaster"; -// import { OrgRoot } from "../entities/OrgRoot"; -import { - createUser, - getUser, - getUserByUsername, - updateUserAttributes, - deleteUser, - getRoles, - addUserRoles, - getAllUsersPaginated, -} from "../keycloak"; -import { OrgRevision } from "../entities/OrgRevision"; - -export interface UserProfileAttributes { - profileId: string | null; - orgRootDnaId: string | null; - orgChild1DnaId: string | null; - orgChild2DnaId: string | null; - orgChild3DnaId: string | null; - orgChild4DnaId: string | null; - empType: string | null; - prefix?: string | null; -} - -/** - * Keycloak Attribute Service - * Service for syncing profileId and orgRootDnaId to Keycloak user attributes - */ -export class KeycloakAttributeService { - private profileRepo = AppDataSource.getRepository(Profile); - private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); - // private posMasterRepo = AppDataSource.getRepository(PosMaster); - // private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); - // private orgRootRepo = AppDataSource.getRepository(OrgRoot); - private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); - - /** - * Get profile attributes (profileId and orgRootDnaId) from database - * Searches in Profile table first (ข้าราชการ), then ProfileEmployee (ลูกจ้าง) - * - * @param keycloakUserId - Keycloak user ID - * @returns UserProfileAttributes with profileId and orgRootDnaId - */ - async getUserProfileAttributes(keycloakUserId: string): Promise { - // First, try to find in Profile (ข้าราชการ) - const revisionCurrent = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true }, - }); - const revisionId = revisionCurrent ? revisionCurrent.id : null; - const profileResult = await this.profileRepo - .createQueryBuilder("p") - .leftJoinAndSelect("p.current_holders", "pm") - .leftJoinAndSelect("pm.orgRoot", "orgRoot") - .leftJoinAndSelect("pm.orgChild1", "orgChild1") - .leftJoinAndSelect("pm.orgChild2", "orgChild2") - .leftJoinAndSelect("pm.orgChild3", "orgChild3") - .leftJoinAndSelect("pm.orgChild4", "orgChild4") - .where("p.keycloak = :keycloakUserId", { keycloakUserId }) - .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) - .getOne(); - - if ( - profileResult && - profileResult.current_holders && - profileResult.current_holders.length > 0 - ) { - const currentPos = profileResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - return { - profileId: profileResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: "OFFICER", - prefix: profileResult.prefix, - }; - } - - // If not found in Profile, try ProfileEmployee (ลูกจ้าง) - const profileEmployeeResult = await this.profileEmployeeRepo - .createQueryBuilder("pe") - .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") - .leftJoinAndSelect("epm.orgChild1", "orgChild1") - .leftJoinAndSelect("epm.orgChild2", "orgChild2") - .leftJoinAndSelect("epm.orgChild3", "orgChild3") - .leftJoinAndSelect("epm.orgChild4", "orgChild4") - .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) - .getOne(); - - if ( - profileEmployeeResult && - profileEmployeeResult.current_holders && - profileEmployeeResult.current_holders.length > 0 - ) { - const currentPos = profileEmployeeResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - return { - profileId: profileEmployeeResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: profileEmployeeResult.employeeClass, - prefix: profileEmployeeResult.prefix, - }; - } - - // Return null values if no profile found - return { - profileId: null, - orgRootDnaId: null, - orgChild1DnaId: null, - orgChild2DnaId: null, - orgChild3DnaId: null, - orgChild4DnaId: null, - empType: null, - prefix: null, - }; - } - - /** - * Get profile attributes by profile ID directly - * Used for syncing specific profiles - * - * @param profileId - Profile ID - * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง - * @returns UserProfileAttributes with profileId and orgRootDnaId - */ - async getAttributesByProfileId( - profileId: string, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise { - const revisionCurrent = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true }, - }); - const revisionId = revisionCurrent ? revisionCurrent.id : null; - if (profileType === "PROFILE") { - const profileResult = await this.profileRepo - .createQueryBuilder("p") - .leftJoinAndSelect("p.current_holders", "pm") - .leftJoinAndSelect("pm.orgRoot", "orgRoot") - .leftJoinAndSelect("pm.orgChild1", "orgChild1") - .leftJoinAndSelect("pm.orgChild2", "orgChild2") - .leftJoinAndSelect("pm.orgChild3", "orgChild3") - .leftJoinAndSelect("pm.orgChild4", "orgChild4") - .where("p.id = :profileId", { profileId }) - .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) - .getOne(); - - if ( - profileResult && - profileResult.current_holders && - profileResult.current_holders.length > 0 - ) { - const currentPos = profileResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - return { - profileId: profileResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: "OFFICER", - prefix: profileResult.prefix, - }; - } - } else { - const profileEmployeeResult = await this.profileEmployeeRepo - .createQueryBuilder("pe") - .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") - .leftJoinAndSelect("pm.orgChild1", "orgChild1") - .leftJoinAndSelect("pm.orgChild2", "orgChild2") - .leftJoinAndSelect("pm.orgChild3", "orgChild3") - .leftJoinAndSelect("pm.orgChild4", "orgChild4") - .where("pe.id = :profileId", { profileId }) - .getOne(); - - if ( - profileEmployeeResult && - profileEmployeeResult.current_holders && - profileEmployeeResult.current_holders.length > 0 - ) { - const currentPos = profileEmployeeResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - - return { - profileId: profileEmployeeResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: profileEmployeeResult.employeeClass, - prefix: profileEmployeeResult.prefix, - }; - } - } - - return { - profileId: null, - orgRootDnaId: null, - orgChild1DnaId: null, - orgChild2DnaId: null, - orgChild3DnaId: null, - orgChild4DnaId: null, - empType: null, - prefix: null, - }; - } - - /** - * Sync user attributes to Keycloak - * - * @param keycloakUserId - Keycloak user ID - * @returns true if sync successful, false otherwise - */ - async syncUserAttributes(keycloakUserId: string): Promise { - try { - const attributes = await this.getUserProfileAttributes(keycloakUserId); - - if (!attributes.profileId) { - console.log(`No profile found for Keycloak user ${keycloakUserId}`); - return false; - } - - // Prepare attributes for Keycloak (must be arrays) - const keycloakAttributes: Record = { - profileId: [attributes.profileId], - orgRootDnaId: [attributes.orgRootDnaId || ""], - orgChild1DnaId: [attributes.orgChild1DnaId || ""], - orgChild2DnaId: [attributes.orgChild2DnaId || ""], - orgChild3DnaId: [attributes.orgChild3DnaId || ""], - orgChild4DnaId: [attributes.orgChild4DnaId || ""], - empType: [attributes.empType || ""], - prefix: [attributes.prefix || ""], - }; - - const success = await updateUserAttributes(keycloakUserId, keycloakAttributes); - - if (success) { - console.log(`Synced attributes for Keycloak user ${keycloakUserId}:`, attributes); - } - - return success; - } catch (error) { - console.error(`Error syncing attributes for Keycloak user ${keycloakUserId}:`, error); - return false; - } - } - - /** - * Sync attributes when organization changes - * This is called when a user moves to a different organization - * - * @param profileId - Profile ID - * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง - * @returns true if sync successful, false otherwise - */ - async syncOnOrganizationChange( - profileId: string, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise { - try { - // Get the keycloak userId from the profile - let keycloakUserId: string | null = null; - - if (profileType === "PROFILE") { - const profile = await this.profileRepo.findOne({ where: { id: profileId } }); - keycloakUserId = profile?.keycloak || ""; - } else { - const profileEmployee = await this.profileEmployeeRepo.findOne({ - where: { id: profileId }, - }); - keycloakUserId = profileEmployee?.keycloak || ""; - } - - if (!keycloakUserId) { - console.log(`No Keycloak user ID found for profile ${profileId}`); - return false; - } - - return await this.syncUserAttributes(keycloakUserId); - } catch (error) { - console.error(`Error syncing organization change for profile ${profileId}:`, error); - return false; - } - } - - /** - * Batch sync multiple users with unlimited count and parallel processing - * Useful for initial sync or periodic updates - * - * @param options - Optional configuration (limit for testing, concurrency for parallel processing) - * @returns Object with success count and details - */ - async batchSyncUsers(options?: { - limit?: number; - concurrency?: number; - }): Promise<{ total: number; success: number; failed: number; details: any[] }> { - const limit = options?.limit; - const concurrency = options?.concurrency ?? 5; - - const result = { - total: 0, - success: 0, - failed: 0, - details: [] as any[], - }; - - try { - // Build query for profiles with keycloak IDs (ข้าราชการ) - const profileQuery = this.profileRepo - .createQueryBuilder("p") - .where("p.keycloak IS NOT NULL") - .andWhere("p.keycloak != :empty", { empty: "" }); - - // Build query for profileEmployees with keycloak IDs (ลูกจ้าง) - const profileEmployeeQuery = this.profileEmployeeRepo - .createQueryBuilder("pe") - .where("pe.keycloak IS NOT NULL") - .andWhere("pe.keycloak != :empty", { empty: "" }); - - // Apply limit if specified (for testing purposes) - if (limit !== undefined) { - profileQuery.take(limit); - profileEmployeeQuery.take(limit); - } - - // Get profiles from both tables - const [profiles, profileEmployees] = await Promise.all([ - profileQuery.getMany(), - profileEmployeeQuery.getMany(), - ]); - - const allProfiles = [ - ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), - ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), - ]; - - result.total = allProfiles.length; - - // Process in parallel with concurrency limit - const processedResults = await this.processInParallel( - allProfiles, - concurrency, - async ({ profile, type }, _index) => { - const keycloakUserId = profile.keycloak; - - try { - const success = await this.syncOnOrganizationChange(profile.id, type); - if (success) { - result.success++; - return { - profileId: profile.id, - keycloakUserId, - status: "success", - }; - } else { - result.failed++; - return { - profileId: profile.id, - keycloakUserId, - status: "failed", - error: "Sync returned false", - }; - } - } catch (error: any) { - result.failed++; - return { - profileId: profile.id, - keycloakUserId, - status: "error", - error: error.message, - }; - } - }, - ); - - // Separate results from errors - for (const resultItem of processedResults) { - if ("error" in resultItem) { - result.failed++; - result.details.push({ - profileId: "unknown", - keycloakUserId: "unknown", - status: "error", - error: JSON.stringify(resultItem.error), - }); - } else { - result.details.push(resultItem); - } - } - - console.log( - `Batch sync completed: total=${result.total}, success=${result.success}, failed=${result.failed}`, - ); - } catch (error) { - console.error("Error in batch sync:", error); - } - - return result; - } - - /** - * Get current Keycloak attributes for a user - * - * @param keycloakUserId - Keycloak user ID - * @returns Current attributes from Keycloak - */ - async getCurrentKeycloakAttributes( - keycloakUserId: string, - ): Promise { - try { - const user = await getUser(keycloakUserId); - - if (!user || !user.attributes) { - return null; - } - - return { - profileId: user.attributes.profileId?.[0] || "", - orgRootDnaId: user.attributes.orgRootDnaId?.[0] || "", - orgChild1DnaId: user.attributes.orgChild1DnaId?.[0] || "", - orgChild2DnaId: user.attributes.orgChild2DnaId?.[0] || "", - orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "", - orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "", - empType: user.attributes.empType?.[0] || "", - prefix: user.attributes.prefix?.[0] || "", - }; - } catch (error) { - console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error); - return null; - } - } - - /** - * Ensure Keycloak user exists for a profile - * Creates user if keycloak field is empty OR if stored keycloak ID doesn't exist in Keycloak - * - * @param profileId - Profile ID - * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' - * @returns Object with status and details - */ - async ensureKeycloakUser( - profileId: string, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise<{ - success: boolean; - action: "created" | "verified" | "skipped" | "error"; - keycloakUserId?: string; - error?: string; - }> { - try { - // Get profile from database - let profile: Profile | ProfileEmployee | null = null; - - if (profileType === "PROFILE") { - profile = await this.profileRepo.findOne({ where: { id: profileId } }); - } else { - profile = await this.profileEmployeeRepo.findOne({ where: { id: profileId } }); - } - - if (!profile) { - return { - success: false, - action: "error", - error: `Profile ${profileId} not found in database`, - }; - } - - // Check if citizenId exists - if (!profile.citizenId) { - return { - success: false, - action: "skipped", - error: "No citizenId found", - }; - } - - // Case 1: keycloak field is empty -> create new user - if (!profile.keycloak || profile.keycloak.trim() === "") { - const result = await this.createKeycloakUserFromProfile(profile, profileType); - return result; - } - - // Case 2: keycloak field is not empty -> verify user exists in Keycloak - const existingUser = await getUser(profile.keycloak); - - if (!existingUser) { - // User doesn't exist in Keycloak, create new one - console.log( - `Keycloak user ${profile.keycloak} not found in Keycloak, creating new user for profile ${profileId}`, - ); - const result = await this.createKeycloakUserFromProfile(profile, profileType); - return result; - } - - // User exists in Keycloak, verified - return { - success: true, - action: "verified", - keycloakUserId: profile.keycloak, - }; - } catch (error: any) { - console.error(`Error ensuring Keycloak user for profile ${profileId}:`, error); - return { - success: false, - action: "error", - error: error.message || "Unknown error", - }; - } - } - - /** - * Create Keycloak user from profile data - * - * @param profile - Profile or ProfileEmployee entity - * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' - * @returns Object with status and details - */ - private async createKeycloakUserFromProfile( - profile: Profile | ProfileEmployee, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise<{ - success: boolean; - action: "created" | "verified" | "skipped" | "error"; - keycloakUserId?: string; - error?: string; - }> { - try { - // Check if user already exists by username (citizenId) - const existingUserByUsername = await getUserByUsername(profile.citizenId); - if (Array.isArray(existingUserByUsername) && existingUserByUsername.length > 0) { - // User already exists with this username, update the keycloak field - const existingUserId = existingUserByUsername[0].id; - console.log( - `User with citizenId ${profile.citizenId} already exists in Keycloak with ID ${existingUserId}`, - ); - - // Update the keycloak field in database - if (profileType === "PROFILE") { - await this.profileRepo.update(profile.id, { keycloak: existingUserId }); - } else { - await this.profileEmployeeRepo.update(profile.id, { keycloak: existingUserId }); - } - - // Assign default USER role to existing user - const userRole = await getRoles("USER"); - if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { - const roleAssigned = await addUserRoles(existingUserId, [ - { id: String(userRole.id), name: String(userRole.name) }, - ]); - if (roleAssigned) { - console.log(`Assigned USER role to existing user ${existingUserId}`); - } else { - console.warn(`Failed to assign USER role to existing user ${existingUserId}`); - } - } else { - console.warn(`USER role not found in Keycloak`); - } - - return { - success: true, - action: "verified", - keycloakUserId: existingUserId, - }; - } - - // Create new user in Keycloak - const createResult = await createUser(profile.citizenId, "P@ssw0rd", { - firstName: profile.firstName || "", - lastName: profile.lastName || "", - email: profile.email || undefined, - enabled: true, - }); - - if (!createResult || typeof createResult !== "string") { - return { - success: false, - action: "error", - error: "Failed to create user in Keycloak", - }; - } - - const keycloakUserId = createResult; - - // Update the keycloak field in database - if (profileType === "PROFILE") { - await this.profileRepo.update(profile.id, { keycloak: keycloakUserId }); - } else { - await this.profileEmployeeRepo.update(profile.id, { keycloak: keycloakUserId }); - } - - // Assign default USER role - const userRole = await getRoles("USER"); - if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { - const roleAssigned = await addUserRoles(keycloakUserId, [ - { id: String(userRole.id), name: String(userRole.name) }, - ]); - if (roleAssigned) { - console.log(`Assigned USER role to user ${keycloakUserId}`); - } else { - console.warn(`Failed to assign USER role to user ${keycloakUserId}`); - } - } else { - console.warn(`USER role not found in Keycloak`); - } - - console.log( - `Created Keycloak user for profile ${profile.id} (citizenId: ${profile.citizenId}) with ID ${keycloakUserId}`, - ); - - return { - success: true, - action: "created", - keycloakUserId, - }; - } catch (error: any) { - console.error(`Error creating Keycloak user for profile ${profile.id}:`, error); - return { - success: false, - action: "error", - error: error.message || "Unknown error", - }; - } - } - - /** - * Process items in parallel with concurrency limit - */ - private async processInParallel( - items: T[], - concurrencyLimit: number, - processor: (item: T, index: number) => Promise, - ): Promise> { - const results: Array = []; - - // Process items in batches - for (let i = 0; i < items.length; i += concurrencyLimit) { - const batch = items.slice(i, i + concurrencyLimit); - - // Process batch in parallel with error handling - const batchResults = await Promise.all( - batch.map(async (item, batchIndex) => { - try { - return await processor(item, i + batchIndex); - } catch (error) { - return { error }; - } - }), - ); - - results.push(...batchResults); - - // Log progress after each batch - const completed = Math.min(i + concurrencyLimit, items.length); - console.log(`Progress: ${completed}/${items.length}`); - } - - return results; - } - - /** - * Batch ensure Keycloak users for all profiles - * Processes all rows in Profile and ProfileEmployee tables - * - * @returns Object with total, success, failed counts and details - */ - async batchEnsureKeycloakUsers(): Promise<{ - total: number; - created: number; - verified: number; - skipped: number; - failed: number; - details: Array<{ - profileId: string; - profileType: string; - action: string; - keycloakUserId?: string; - error?: string; - }>; - }> { - const result = { - total: 0, - created: 0, - verified: 0, - skipped: 0, - failed: 0, - details: [] as Array<{ - profileId: string; - profileType: string; - action: string; - keycloakUserId?: string; - error?: string; - }>, - }; - - try { - // Get all profiles from Profile table (ข้าราชการ) - const profiles = await this.profileRepo.find({ where: { isLeave: false } }); // Only active profiles - - // Get all profiles from ProfileEmployee table (ลูกจ้าง) - const profileEmployees = await this.profileEmployeeRepo.find({ where: { isLeave: false } }); // Only active profiles - - const allProfiles = [ - ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), - ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), - ]; - - result.total = allProfiles.length; - - // Process in parallel with concurrency limit - const CONCURRENCY_LIMIT = 5; // Adjust based on environment - - const processedResults = await this.processInParallel( - allProfiles, - CONCURRENCY_LIMIT, - async ({ profile, type }) => { - const ensureResult = await this.ensureKeycloakUser(profile.id, type); - - // Update counters - switch (ensureResult.action) { - case "created": - result.created++; - break; - case "verified": - result.verified++; - break; - case "skipped": - result.skipped++; - break; - case "error": - result.failed++; - break; - } - - return { - profileId: profile.id, - profileType: type, - action: ensureResult.action, - keycloakUserId: ensureResult.keycloakUserId, - error: ensureResult.error, - }; - }, - ); - - // Separate results from errors - for (const resultItem of processedResults) { - if ("error" in resultItem) { - result.failed++; - result.details.push({ - profileId: "unknown", - profileType: "unknown", - action: "error", - error: JSON.stringify(resultItem.error), - }); - } else { - result.details.push(resultItem); - } - } - - console.log( - `Batch ensure Keycloak users completed: total=${result.total}, created=${result.created}, verified=${result.verified}, skipped=${result.skipped}, failed=${result.failed}`, - ); - } catch (error) { - console.error("Error in batch ensure Keycloak users:", error); - } - - return result; - } - - /** - * Clear orphaned Keycloak users - * Deletes users in Keycloak that are not referenced in Profile or ProfileEmployee tables - * - * @param skipUsernames - Array of usernames to skip (e.g., ['super_admin']) - * @returns Object with counts and details - */ - async clearOrphanedKeycloakUsers(skipUsernames: string[] = []): Promise<{ - totalInKeycloak: number; - totalInDatabase: number; - orphanedCount: number; - deleted: number; - skipped: number; - failed: number; - details: Array<{ - keycloakUserId: string; - username: string; - action: "deleted" | "skipped" | "error"; - error?: string; - }>; - }> { - const result = { - totalInKeycloak: 0, - totalInDatabase: 0, - orphanedCount: 0, - deleted: 0, - skipped: 0, - failed: 0, - details: [] as Array<{ - keycloakUserId: string; - username: string; - action: "deleted" | "skipped" | "error"; - error?: string; - }>, - }; - - try { - // Get all keycloak IDs from database (Profile + ProfileEmployee) - const profiles = await this.profileRepo - .createQueryBuilder("p") - .where("p.keycloak IS NOT NULL") - .andWhere("p.keycloak != :empty", { empty: "" }) - .getMany(); - - const profileEmployees = await this.profileEmployeeRepo - .createQueryBuilder("pe") - .where("pe.keycloak IS NOT NULL") - .andWhere("pe.keycloak != :empty", { empty: "" }) - .getMany(); - - // Create a Set of all keycloak IDs in database for O(1) lookup - const databaseKeycloakIds = new Set(); - for (const p of profiles) { - if (p.keycloak) databaseKeycloakIds.add(p.keycloak); - } - for (const pe of profileEmployees) { - if (pe.keycloak) databaseKeycloakIds.add(pe.keycloak); - } - - result.totalInDatabase = databaseKeycloakIds.size; - - // Get all users from Keycloak with pagination to avoid response size limits - const keycloakUsers = await getAllUsersPaginated(); - if (!keycloakUsers || typeof keycloakUsers !== "object") { - throw new Error("Failed to get users from Keycloak"); - } - - result.totalInKeycloak = keycloakUsers.length; - - // Find orphaned users (in Keycloak but not in database) - const orphanedUsers = keycloakUsers.filter((user: any) => !databaseKeycloakIds.has(user.id)); - result.orphanedCount = orphanedUsers.length; - - // Delete orphaned users (skip protected ones) - for (const user of orphanedUsers) { - const username = user.username; - const userId = user.id; - - // Check if user should be skipped - if (skipUsernames.includes(username)) { - result.skipped++; - result.details.push({ - keycloakUserId: userId, - username, - action: "skipped", - }); - continue; - } - - // Delete user from Keycloak - try { - const deleteSuccess = await deleteUser(userId); - if (deleteSuccess) { - result.deleted++; - result.details.push({ - keycloakUserId: userId, - username, - action: "deleted", - }); - } else { - result.failed++; - result.details.push({ - keycloakUserId: userId, - username, - action: "error", - error: "Failed to delete user from Keycloak", - }); - } - } catch (error: any) { - result.failed++; - result.details.push({ - keycloakUserId: userId, - username, - action: "error", - error: error.message || "Unknown error", - }); - } - } - - console.log( - `Clear orphaned Keycloak users completed: totalInKeycloak=${result.totalInKeycloak}, totalInDatabase=${result.totalInDatabase}, orphaned=${result.orphanedCount}, deleted=${result.deleted}, skipped=${result.skipped}, failed=${result.failed}`, - ); - } catch (error) { - console.error("Error in clear orphaned Keycloak users:", error); - throw error; - } - - return result; - } -} From d667ad9173d13cc83b67e1f23b6f420499c5ce76 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 23:09:22 +0700 Subject: [PATCH 125/178] fix: sync and script keycloak --- scripts/KEYCLOAK_SYNC_README.md | 149 +++ scripts/assign-user-role.ts | 269 +++++ scripts/clear-orphaned-users.ts | 80 ++ scripts/ensure-users.ts | 91 ++ scripts/sync-all.ts | 93 ++ src/app.ts | 44 +- src/controllers/KeycloakSyncController.ts | 254 +++++ src/controllers/ScriptProfileOrgController.ts | 326 ++++++ src/keycloak/index.ts | 225 ++++- src/middlewares/auth.ts | 10 + src/middlewares/user.ts | 8 + src/services/KeycloakAttributeService.ts | 928 ++++++++++++++++++ 12 files changed, 2444 insertions(+), 33 deletions(-) create mode 100644 scripts/KEYCLOAK_SYNC_README.md create mode 100644 scripts/assign-user-role.ts create mode 100644 scripts/clear-orphaned-users.ts create mode 100644 scripts/ensure-users.ts create mode 100644 scripts/sync-all.ts create mode 100644 src/controllers/KeycloakSyncController.ts create mode 100644 src/controllers/ScriptProfileOrgController.ts create mode 100644 src/services/KeycloakAttributeService.ts diff --git a/scripts/KEYCLOAK_SYNC_README.md b/scripts/KEYCLOAK_SYNC_README.md new file mode 100644 index 00000000..4c61ec75 --- /dev/null +++ b/scripts/KEYCLOAK_SYNC_README.md @@ -0,0 +1,149 @@ +# Keycloak Sync Scripts + +This directory contains standalone scripts for managing Keycloak users from the CLI. These scripts are useful for maintenance, setup, and troubleshooting Keycloak synchronization. + +## Prerequisites + +- Node.js and TypeScript installed +- Database connection configured in `.env` +- Keycloak connection configured in `.env` +- Run with `ts-node` or compile first + +## Environment Variables + +Ensure these are set in your `.env` file: + +```bash +# Database +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=your_database +DB_USER=your_user +DB_PASSWORD=your_password + +# Keycloak +KC_URL=https://your-keycloak-url +KC_REALMS=your-realm +KC_SERVICE_ACCOUNT_CLIENT_ID=your-client-id +KC_SERVICE_ACCOUNT_SECRET=your-client-secret +``` + +## Scripts + +### 1. clear-orphaned-users.ts + +Deletes Keycloak users that don't exist in the database (Profile + ProfileEmployee tables). Useful for cleaning up invalid users. + +**Protected usernames** (never deleted): `super_admin`, `admin_issue` + +```bash +# Dry run (preview changes) +ts-node scripts/clear-orphaned-users.ts --dry-run + +# Live execution +ts-node scripts/clear-orphaned-users.ts +``` + +**Output:** +- Total users in Keycloak +- Total users in Database +- Orphaned users found +- Deleted/Skipped/Failed counts + +### 2. ensure-users.ts + +Checks and creates Keycloak users for all profiles. Includes role assignment (USER role) automatically. + +```bash +# Dry run (preview changes) +ts-node scripts/ensure-users.ts --dry-run + +# Live execution +ts-node scripts/ensure-users.ts + +# Test with limited profiles (for testing) +ts-node scripts/ensure-users.ts --dry-run --limit=10 +``` + +**Output:** +- Total profiles processed +- Created users (new Keycloak accounts) +- Verified users (already exists) +- Skipped profiles (no citizenId) +- Failed count + +### 3. sync-all.ts + +Syncs all attributes to Keycloak for users with existing Keycloak IDs. + +**Attributes synced:** +- `profileId` +- `orgRootDnaId` +- `orgChild1DnaId` +- `orgChild2DnaId` +- `orgChild3DnaId` +- `orgChild4DnaId` +- `empType` +- `prefix` + +```bash +# Dry run (preview changes) +ts-node scripts/sync-all.ts --dry-run + +# Live execution +ts-node scripts/sync-all.ts + +# Test with limited profiles +ts-node scripts/sync-all.ts --dry-run --limit=10 + +# Adjust concurrency (default: 5) +ts-node scripts/sync-all.ts --concurrency=10 +``` + +**Output:** +- Total profiles with Keycloak IDs +- Success/Failed counts +- Error details for failures + +## Recommended Execution Order + +For initial setup or full resynchronization: + +1. **clear-orphaned-users** - Clean up invalid users first + ```bash + npx ts-node scripts/clear-orphaned-users.ts + ``` + +2. **ensure-users** - Create missing users and assign roles + ```bash + npx ts-node scripts/ensure-users.ts + ``` + +3. **sync-all** - Sync all attributes to Keycloak + ```bash + npx ts-node scripts/sync-all.ts + ``` + +## Tips + +- Always run with `--dry-run` first to preview changes +- Use `--limit=N` for testing before running on full dataset +- Scripts process both Profile (ข้าราชการ) and ProfileEmployee (ลูกจ้าง) tables +- Only active profiles (`isLeave: false`) are processed by ensure-users +- The `USER` role is automatically assigned to new/verified users + +## Troubleshooting + +**Database connection error:** +- Check `.env` database variables +- Ensure database server is running + +**Keycloak connection error:** +- Check `KC_URL`, `KC_REALMS` in `.env` +- Verify service account credentials +- Check network connectivity to Keycloak + +**USER role not found:** +- Log in to Keycloak admin console +- Create a `USER` role in your realm +- Ensure service account has `manage-users` and `view-users` permissions \ No newline at end of file diff --git a/scripts/assign-user-role.ts b/scripts/assign-user-role.ts new file mode 100644 index 00000000..4161b31b --- /dev/null +++ b/scripts/assign-user-role.ts @@ -0,0 +1,269 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { Profile } from "../src/entities/Profile"; +import { ProfileEmployee } from "../src/entities/ProfileEmployee"; +import * as keycloak from "../src/keycloak/index"; + +const USER_ROLE_NAME = "USER"; + +interface AssignOptions { + dryRun: boolean; + targetUsernames?: string[]; +} + +interface UserWithKeycloak { + keycloakId: string; + citizenId: string; + source: "Profile" | "ProfileEmployee"; +} + +interface AssignResult { + total: number; + assigned: number; + skipped: number; + failed: number; + errors: Array<{ + userId: string; + username: string; + error: string; + }>; +} + +/** + * Get all users from database who have Keycloak IDs set + */ +async function getUsersWithKeycloak(): Promise { + const users: UserWithKeycloak[] = []; + const profileRepo = AppDataSource.getRepository(Profile); + const profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); + + // Get from Profile table + const profiles = await profileRepo + .createQueryBuilder("profile") + .where("profile.keycloak IS NOT NULL") + .andWhere("profile.keycloak != ''") + .getMany(); + + for (const profile of profiles) { + users.push({ + keycloakId: profile.keycloak, + citizenId: profile.citizenId || profile.id, + source: "Profile", + }); + } + + // Get from ProfileEmployee table + const employees = await profileEmployeeRepo + .createQueryBuilder("profileEmployee") + .where("profileEmployee.keycloak IS NOT NULL") + .andWhere("profileEmployee.keycloak != ''") + .getMany(); + + for (const employee of employees) { + // Avoid duplicates - check if keycloak ID already exists + if (!users.some((u) => u.keycloakId === employee.keycloak)) { + users.push({ + keycloakId: employee.keycloak, + citizenId: employee.citizenId || employee.id, + source: "ProfileEmployee", + }); + } + } + + return users; +} + +/** + * Assign USER role to users who don't have it + */ +async function assignUserRoleToUsers( + users: UserWithKeycloak[], + userRoleId: string, + options: AssignOptions, +): Promise { + const result: AssignResult = { + total: users.length, + assigned: 0, + skipped: 0, + failed: 0, + errors: [], + }; + + console.log(`Processing ${result.total} users...`); + + for (let i = 0; i < users.length; i++) { + const user = users[i]; + const index = i + 1; + + try { + // Get user's current roles + const userRoles = await keycloak.getUserRoles(user.keycloakId); + + if (!userRoles || typeof userRoles === "boolean") { + console.log( + `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Failed to get roles`, + ); + result.failed++; + result.errors.push({ + userId: user.keycloakId, + username: user.citizenId, + error: "Failed to get user roles", + }); + continue; + } + + // Handle both array and single object return types + // getUserRoles can return an array or a single object + const rolesArray = Array.isArray(userRoles) ? userRoles : [userRoles]; + + // Check if user already has USER role + const hasUserRole = rolesArray.some( + (role: { id: string; name: string }) => role.name === USER_ROLE_NAME, + ); + + if (hasUserRole) { + console.log( + `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Already has USER role`, + ); + result.skipped++; + continue; + } + + // Assign USER role + if (options.dryRun) { + console.log( + `[${index}/${result.total}] [DRY-RUN] Would assign USER role: ${user.citizenId} (source: ${user.source})`, + ); + result.assigned++; + } else { + const assignResult = await keycloak.addUserRoles(user.keycloakId, [ + { id: userRoleId, name: USER_ROLE_NAME }, + ]); + + if (assignResult) { + console.log( + `[${index}/${result.total}] Assigned USER role: ${user.citizenId} (source: ${user.source})`, + ); + result.assigned++; + } else { + console.log( + `[${index}/${result.total}] Failed: ${user.citizenId} (source: ${user.source}) - Could not assign role`, + ); + result.failed++; + result.errors.push({ + userId: user.keycloakId, + username: user.citizenId, + error: "Failed to assign USER role", + }); + } + } + + // Small delay to avoid rate limiting + if (index % 50 === 0) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.log( + `[${index}/${result.total}] Error: ${user.citizenId} (source: ${user.source}) - ${errorMessage}`, + ); + result.failed++; + result.errors.push({ + userId: user.keycloakId, + username: user.citizenId, + error: errorMessage, + }); + } + } + + return result; +} + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + + console.log("=".repeat(60)); + console.log("Keycloak USER Role Assignment Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + // Get USER role from Keycloak + console.log(""); + const userRole = await keycloak.getRoles(USER_ROLE_NAME); + + // Check if USER role exists and is valid (has id property) + if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { + console.error(`ERROR: ${USER_ROLE_NAME} role not found in Keycloak`); + await AppDataSource.destroy(); + process.exit(1); + } + + // Type assertion via unknown to bypass union type issues + const role = userRole as unknown as { id: string; name: string; description?: string }; + const roleId = role.id; + console.log(`${USER_ROLE_NAME} role found: ${roleId}`); + console.log(""); + + // Get users from database + console.log("Fetching users from database..."); + let users = await getUsersWithKeycloak(); + + if (users.length === 0) { + console.log("No users with Keycloak IDs found in database"); + await AppDataSource.destroy(); + process.exit(0); + } + + console.log(`Found ${users.length} users with Keycloak IDs`); + console.log(""); + + // Assign USER role + const result = await assignUserRoleToUsers(users, roleId, { dryRun }); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total users: ${result.total}`); + console.log(` Assigned: ${result.assigned}`); + console.log(` Skipped: ${result.skipped}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.errors.length > 0) { + console.log(""); + console.log("Errors:"); + result.errors.forEach((e) => { + console.log(` ${e.username}: ${e.error}`); + }); + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/scripts/clear-orphaned-users.ts b/scripts/clear-orphaned-users.ts new file mode 100644 index 00000000..67eee99d --- /dev/null +++ b/scripts/clear-orphaned-users.ts @@ -0,0 +1,80 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; +import * as keycloak from "../src/keycloak/index"; + +const PROTECTED_USERNAMES = ["super_admin", "admin_issue"]; + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const skipUsernames = [...PROTECTED_USERNAMES]; + + console.log("=".repeat(60)); + console.log("Clear Orphaned Keycloak Users Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + console.log(`Protected usernames: ${skipUsernames.join(", ")}`); + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log(""); + + // Run the orphaned user cleanup + const service = new KeycloakAttributeService(); + const result = await service.clearOrphanedKeycloakUsers(skipUsernames); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total users in Keycloak: ${result.totalInKeycloak}`); + console.log(` Total users in Database: ${result.totalInDatabase}`); + console.log(` Orphaned users: ${result.orphanedCount}`); + console.log(` Deleted: ${result.deleted}`); + console.log(` Skipped: ${result.skipped}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.details.length > 0) { + console.log(""); + console.log("Details:"); + for (const detail of result.details) { + const status = + detail.action === "deleted" + ? "[DELETED]" + : detail.action === "skipped" + ? "[SKIPPED]" + : "[ERROR]"; + console.log( + ` ${status} ${detail.username} (${detail.keycloakUserId})${detail.error ? ": " + detail.error : ""}`, + ); + } + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/scripts/ensure-users.ts b/scripts/ensure-users.ts new file mode 100644 index 00000000..14522d96 --- /dev/null +++ b/scripts/ensure-users.ts @@ -0,0 +1,91 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; +import * as keycloak from "../src/keycloak/index"; + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const limitArg = args.find((arg) => arg.startsWith("--limit=")); + const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; + + console.log("=".repeat(60)); + console.log("Ensure Keycloak Users Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + if (limit !== undefined) { + console.log(`Limit: ${limit} profiles per table (for testing)`); + } + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log(""); + + // Verify USER role exists + console.log("Verifying USER role in Keycloak..."); + const userRole = await keycloak.getRoles("USER"); + + if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { + console.error("ERROR: USER role not found in Keycloak"); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log("USER role found"); + console.log(""); + + // Run the ensure users operation + const service = new KeycloakAttributeService(); + console.log("Ensuring Keycloak users for all profiles..."); + console.log(""); + + const result = await service.batchEnsureKeycloakUsers(); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total profiles: ${result.total}`); + console.log(` Created: ${result.created}`); + console.log(` Verified: ${result.verified}`); + console.log(` Skipped: ${result.skipped}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.failed > 0) { + console.log(""); + console.log("Failed Details:"); + const failedDetails = result.details.filter((d) => d.action === "error" || !!d.error); + for (const detail of failedDetails) { + console.log( + ` [${detail.profileType}] ${detail.profileId}: ${detail.error || "Unknown error"}`, + ); + } + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/scripts/sync-all.ts b/scripts/sync-all.ts new file mode 100644 index 00000000..9090dd17 --- /dev/null +++ b/scripts/sync-all.ts @@ -0,0 +1,93 @@ +import "dotenv/config"; +import { AppDataSource } from "../src/database/data-source"; +import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; +import * as keycloak from "../src/keycloak/index"; + +/** + * Main function + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const limitArg = args.find((arg) => arg.startsWith("--limit=")); + const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; + const concurrencyArg = args.find((arg) => arg.startsWith("--concurrency=")); + const concurrency = concurrencyArg ? parseInt(concurrencyArg.split("=")[1], 10) : undefined; + + console.log("=".repeat(60)); + console.log("Sync All Attributes to Keycloak Script"); + console.log("=".repeat(60)); + console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); + if (limit !== undefined) { + console.log(`Limit: ${limit} profiles per table (for testing)`); + } + if (concurrency !== undefined) { + console.log(`Concurrency: ${concurrency}`); + } + console.log(""); + + console.log("Attributes to sync:"); + console.log(" - profileId"); + console.log(" - orgRootDnaId"); + console.log(" - orgChild1DnaId"); + console.log(" - orgChild2DnaId"); + console.log(" - orgChild3DnaId"); + console.log(" - orgChild4DnaId"); + console.log(" - empType"); + console.log(" - prefix"); + console.log(""); + + // Initialize database + try { + await AppDataSource.initialize(); + console.log("Database connected"); + } catch (error) { + console.error("Failed to connect to database:", error); + process.exit(1); + } + + // Validate Keycloak connection + try { + await keycloak.getToken(); + console.log("Keycloak connected"); + } catch (error) { + console.error("Failed to connect to Keycloak:", error); + await AppDataSource.destroy(); + process.exit(1); + } + + console.log(""); + + // Run the sync operation + const service = new KeycloakAttributeService(); + console.log("Syncing attributes for all profiles with Keycloak IDs..."); + console.log(""); + + const result = await service.batchSyncUsers({ limit, concurrency }); + + // Summary + console.log(""); + console.log("=".repeat(60)); + console.log("Summary:"); + console.log(` Total profiles: ${result.total}`); + console.log(` Success: ${result.success}`); + console.log(` Failed: ${result.failed}`); + console.log("=".repeat(60)); + + if (result.failed > 0) { + console.log(""); + console.log("Failed Details:"); + for (const detail of result.details.filter( + (d) => d.status === "failed" || d.status === "error", + )) { + console.log( + ` ${detail.profileId} (${detail.keycloakUserId}): ${detail.error || "Sync failed"}`, + ); + } + } + + // Cleanup + await AppDataSource.destroy(); +} + +main().catch(console.error); diff --git a/src/app.ts b/src/app.ts index 75d0bfea..29d50749 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,6 +18,7 @@ import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; +import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; async function main() { await AppDataSource.initialize(); @@ -52,19 +53,8 @@ async function main() { const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_PORT = +(process.env.APP_PORT || 3000); - const cronTime = "0 0 3 * * *"; // ตั้งเวลาทุกวันเวลา 03:00:00 - // const cronTime = "*/10 * * * * *"; - cron.schedule(cronTime, async () => { - try { - const orgController = new OrganizationController(); - await orgController.cronjobRevision(); - } catch (error) { - console.error("Error executing function from controller:", error); - } - }); - - const cronTime_command = "0 0 2 * * *"; - // const cronTime_command = "*/10 * * * * *"; + // Cron job for executing command - every day at 00:30:00 + const cronTime_command = "0 30 0 * * *"; cron.schedule(cronTime_command, async () => { try { const commandController = new CommandController(); @@ -74,7 +64,19 @@ async function main() { } }); - const cronTime_Oct = "0 0 1 10 *"; + // Cron job for updating org revision - every day at 01:00:00 + const cronTime = "0 0 1 * * *"; + cron.schedule(cronTime, async () => { + try { + const orgController = new OrganizationController(); + await orgController.cronjobRevision(); + } catch (error) { + console.error("Error executing function from controller:", error); + } + }); + + // Cron job for updating retirement status - every day at 02:00:00 on the 1st of October + const cronTime_Oct = "0 0 2 10 *"; cron.schedule(cronTime_Oct, async () => { try { const commandController = new CommandController(); @@ -84,7 +86,19 @@ async function main() { } }); - const cronTime_Tenure = "0 0 0 * * *"; + // Cron job for updating org DNA - every day at 03:00:00 + const cronTime_UpdateOrg = "0 0 3 * * *"; + cron.schedule(cronTime_UpdateOrg, async () => { + try { + const scriptProfileOrgController = new ScriptProfileOrgController(); + await scriptProfileOrgController.cronjobUpdateOrg({} as any); + } catch (error) { + console.error("Error executing cronjobUpdateOrg:", error); + } + }); + + // Cron job for updating tenure - every day at 04:00:00 + const cronTime_Tenure = "0 0 4 * * *"; cron.schedule(cronTime_Tenure, async () => { try { const profileSalaryController = new ProfileSalaryController(); diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts new file mode 100644 index 00000000..5f814238 --- /dev/null +++ b/src/controllers/KeycloakSyncController.ts @@ -0,0 +1,254 @@ +import { + Controller, + Post, + Get, + Route, + Security, + Tags, + Path, + Request, + Response, + Query, + Body, +} from "tsoa"; +import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; + +@Route("api/v1/org/keycloak-sync") +@Tags("Keycloak Sync") +@Security("bearerAuth") +@Response( + HttpStatus.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถดำเนินการได้ กรุณาลองใหม่ในภายหลัง", +) +export class KeycloakSyncController extends Controller { + private keycloakAttributeService = new KeycloakAttributeService(); + + /** + * Sync attributes for the current logged-in user + * + * @summary Sync profileId and rootDnaId to Keycloak for current user + */ + @Post("sync-me") + async syncCurrentUser(@Request() request: RequestWithUser) { + const keycloakUserId = request.user.sub; + + if (!keycloakUserId) { + throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); + } + + // Get attributes from database before sync + const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); + + const success = await this.keycloakAttributeService.syncUserAttributes(keycloakUserId); + + if (!success) { + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ กรุณาติดต่อผู้ดูแลระบบ", + ); + } + + // Verify sync by fetching attributes from Keycloak after update + const kcAttrsAfter = + await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); + + return new HttpSuccess({ + message: "Sync ข้อมูลสำเร็จ", + syncedToKeycloak: !!kcAttrsAfter?.profileId, + databaseAttributes: dbAttrs, + keycloakAttributesAfter: kcAttrsAfter, + }); + } + + /** + * Get current attributes of the logged-in user + * + * @summary Get current profileId and rootDnaId from Keycloak + */ + // @Get("my-attributes") + async getMyAttributes(@Request() request: RequestWithUser) { + const keycloakUserId = request.user.sub; + + if (!keycloakUserId) { + throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); + } + + const keycloakAttributes = + await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); + const dbAttributes = + await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); + + return new HttpSuccess({ + keycloakAttributes, + databaseAttributes: dbAttributes, + }); + } + + /** + * Sync attributes for a specific profile (Admin only) + * + * @summary Sync profileId and rootDnaId to Keycloak by profile ID (ADMIN) + * + * @param {string} profileId Profile ID + * @param {string} profileType Profile type (PROFILE or PROFILE_EMPLOYEE) + */ + @Post("sync-profile/:profileId") + async syncByProfileId( + @Path() profileId: string, + @Query() profileType: "PROFILE" | "PROFILE_EMPLOYEE" = "PROFILE", + ) { + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + const success = await this.keycloakAttributeService.syncOnOrganizationChange( + profileId, + profileType, + ); + + if (!success) { + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ หรือไม่พบข้อมูล profile", + ); + } + + return new HttpSuccess({ message: "Sync ข้อมูลสำเร็จ" }); + } + + /** + * Batch sync attributes for multiple profiles (Admin only) + * + * @summary Batch sync profileId and rootDnaId to Keycloak for multiple profiles (ADMIN) + * + * @param {request} request Request body containing profileIds array and profileType + */ + // @Post("sync-profiles-batch") + async syncByProfileIds( + @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" }, + ) { + const { profileIds, profileType } = request; + + // Validate profileIds + if (!profileIds || profileIds.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า"); + } + + // Validate profileType + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + const result = { + total: profileIds.length, + success: 0, + failed: 0, + details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>, + }; + + // Process each profileId + for (const profileId of profileIds) { + try { + const success = await this.keycloakAttributeService.syncOnOrganizationChange( + profileId, + profileType, + ); + + if (success) { + result.success++; + result.details.push({ profileId, status: "success" }); + } else { + result.failed++; + result.details.push({ + profileId, + status: "failed", + error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ profileId, status: "failed", error: error.message }); + } + } + + return new HttpSuccess({ + message: "Batch sync เสร็จสิ้น", + ...result, + }); + } + + /** + * Batch sync all users (Admin only) + * + * @summary Batch sync all users to Keycloak without limit (ADMIN) + * + * @description Syncs profileId and orgRootDnaId to Keycloak for all users + * that have a keycloak ID. Uses parallel processing for better performance. + */ + // @Post("sync-all") + async syncAll() { + const result = await this.keycloakAttributeService.batchSyncUsers(); + + return new HttpSuccess({ + message: "Batch sync เสร็จสิ้น", + total: result.total, + success: result.success, + failed: result.failed, + details: result.details, + }); + } + + /** + * Ensure Keycloak users exist for all profiles (Admin only) + * + * @summary Create or verify Keycloak users for all profiles in Profile and ProfileEmployee tables (ADMIN) + * + * @description + * This endpoint will: + * - Create new Keycloak users for profiles without a keycloak ID + * - Create new Keycloak users for profiles where the stored keycloak ID doesn't exist in Keycloak + * - Verify existing Keycloak users + * - Skip profiles without a citizenId + */ + // @Post("ensure-users") + async ensureAllUsers() { + const result = await this.keycloakAttributeService.batchEnsureKeycloakUsers(); + return new HttpSuccess({ + message: "Batch ensure Keycloak users เสร็จสิ้น", + ...result, + }); + } + + /** + * Clear orphaned Keycloak users (Admin only) + * + * @summary Delete Keycloak users that are not in the database (ADMIN) + * + * @description + * This endpoint will: + * - Find users in Keycloak that are not referenced in Profile or ProfileEmployee tables + * - Delete those orphaned users from Keycloak + * - Skip protected users (super_admin, admin_issue) + * + * @param {request} request Request body containing skipUsernames array + */ + // @Post("clear-orphaned-users") + async clearOrphanedUsers(@Body() request?: { skipUsernames?: string[] }) { + const skipUsernames = request?.skipUsernames || ["super_admin", "admin_issue"]; + const result = await this.keycloakAttributeService.clearOrphanedKeycloakUsers(skipUsernames); + return new HttpSuccess({ + message: "Clear orphaned Keycloak users เสร็จสิ้น", + ...result, + }); + } +} diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts new file mode 100644 index 00000000..c8975e43 --- /dev/null +++ b/src/controllers/ScriptProfileOrgController.ts @@ -0,0 +1,326 @@ +import { Controller, Post, Route, Security, Tags, Request } from "tsoa"; +import { AppDataSource } from "../database/data-source"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; +import { MoreThanOrEqual } from "typeorm"; +import { PosMaster } from "./../entities/PosMaster"; +import axios from "axios"; +import { KeycloakSyncController } from "./KeycloakSyncController"; +import { EmployeePosMaster } from "./../entities/EmployeePosMaster"; + +interface OrgUpdatePayload { + profileId: string; + rootDnaId: string | null; + child1DnaId: string | null; + child2DnaId: string | null; + child3DnaId: string | null; + child4DnaId: string | null; + profileType: "PROFILE" | "PROFILE_EMPLOYEE"; +} + +@Route("api/v1/org/script-profile-org") +@Tags("Keycloak Sync") +@Security("bearerAuth") +export class ScriptProfileOrgController extends Controller { + private posMasterRepo = AppDataSource.getRepository(PosMaster); + private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); + + // Idempotency flag to prevent concurrent runs + private isRunning = false; + + // Configurable values + private readonly BATCH_SIZE = parseInt(process.env.CRONJOB_BATCH_SIZE || "100", 10); + private readonly UPDATE_WINDOW_HOURS = parseInt( + process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24", + 10, + ); + + @Post("update-org") + public async cronjobUpdateOrg(@Request() request: RequestWithUser) { + // Idempotency check - prevent concurrent runs + if (this.isRunning) { + console.log("cronjobUpdateOrg: Job already running, skipping this execution"); + return new HttpSuccess({ + message: "Job already running", + skipped: true, + }); + } + + this.isRunning = true; + const startTime = Date.now(); + + try { + const windowStart = new Date(Date.now() - this.UPDATE_WINDOW_HOURS * 60 * 60 * 1000); + + console.log("cronjobUpdateOrg: Starting job", { + windowHours: this.UPDATE_WINDOW_HOURS, + windowStart: windowStart.toISOString(), + batchSize: this.BATCH_SIZE, + }); + + // Query with optimized select - only fetch required fields + const [posMasters, posMasterEmployee] = await Promise.all([ + this.posMasterRepo.find({ + where: { + lastUpdatedAt: MoreThanOrEqual(windowStart), + orgRevision: { + orgRevisionIsCurrent: true, + }, + }, + relations: [ + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], + select: { + id: true, + current_holderId: true, + lastUpdatedAt: true, + orgRevision: { id: true }, + orgRoot: { ancestorDNA: true }, + orgChild1: { ancestorDNA: true }, + orgChild2: { ancestorDNA: true }, + orgChild3: { ancestorDNA: true }, + orgChild4: { ancestorDNA: true }, + current_holder: { id: true }, + }, + }), + this.employeePosMasterRepo.find({ + where: { + lastUpdatedAt: MoreThanOrEqual(windowStart), + orgRevision: { + orgRevisionIsCurrent: true, + }, + }, + relations: [ + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], + select: { + id: true, + current_holderId: true, + lastUpdatedAt: true, + orgRevision: { id: true }, + orgRoot: { ancestorDNA: true }, + orgChild1: { ancestorDNA: true }, + orgChild2: { ancestorDNA: true }, + orgChild3: { ancestorDNA: true }, + orgChild4: { ancestorDNA: true }, + current_holder: { id: true }, + }, + }), + ]); + + console.log("cronjobUpdateOrg: Database query completed", { + posMastersCount: posMasters.length, + employeePosCount: posMasterEmployee.length, + totalRecords: posMasters.length + posMasterEmployee.length, + }); + + // Build payloads with proper profile type tracking + const payloads = this.buildPayloads(posMasters, posMasterEmployee); + + if (payloads.length === 0) { + console.log("cronjobUpdateOrg: No records to process"); + return new HttpSuccess({ + message: "No records to process", + processed: 0, + }); + } + + // Update profile's org structure in leave service by calling API + console.log("cronjobUpdateOrg: Calling leave service API", { + payloadCount: payloads.length, + }); + + await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, { + headers: { + "Content-Type": "application/json", + api_key: process.env.API_KEY, + }, + timeout: 30000, // 30 second timeout + }); + + console.log("cronjobUpdateOrg: Leave service API call successful"); + + // Group profile IDs by type for proper syncing + const profileIdsByType = this.groupProfileIdsByType(payloads); + + // Sync to Keycloak with batching + const keycloakSyncController = new KeycloakSyncController(); + const syncResults = { + total: 0, + success: 0, + failed: 0, + byType: {} as Record, + }; + + // Process each profile type separately + for (const [profileType, profileIds] of Object.entries(profileIdsByType)) { + console.log(`cronjobUpdateOrg: Syncing ${profileType} profiles`, { + count: profileIds.length, + }); + + const batches = this.chunkArray(profileIds, this.BATCH_SIZE); + const typeResult = { total: profileIds.length, success: 0, failed: 0 }; + + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + console.log( + `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`, + { + batchSize: batch.length, + batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min( + (i + 1) * this.BATCH_SIZE, + profileIds.length, + )}`, + }, + ); + + try { + const batchResult: any = await keycloakSyncController.syncByProfileIds({ + profileIds: batch, + profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", + }); + + // Extract result data if available + const resultData = (batchResult as any)?.data || batchResult; + typeResult.success += resultData.success || 0; + typeResult.failed += resultData.failed || 0; + + console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { + success: resultData.success || 0, + failed: resultData.failed || 0, + }); + } catch (error: any) { + console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { + error: error.message, + batchSize: batch.length, + }); + // Count all profiles in failed batch as failed + typeResult.failed += batch.length; + } + } + + syncResults.byType[profileType] = typeResult; + syncResults.total += typeResult.total; + syncResults.success += typeResult.success; + syncResults.failed += typeResult.failed; + } + + const duration = Date.now() - startTime; + console.log("cronjobUpdateOrg: Job completed", { + duration: `${duration}ms`, + processed: payloads.length, + syncResults, + }); + + return new HttpSuccess({ + message: "Update org completed", + processed: payloads.length, + syncResults, + duration: `${duration}ms`, + }); + } catch (error: any) { + const duration = Date.now() - startTime; + console.error("cronjobUpdateOrg: Job failed", { + duration: `${duration}ms`, + error: error.message, + stack: error.stack, + }); + throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"); + } finally { + this.isRunning = false; + } + } + + /** + * Build payloads from PosMaster and EmployeePosMaster records + * Includes proper profile type tracking for accurate Keycloak sync + */ + private buildPayloads( + posMasters: PosMaster[], + posMasterEmployee: EmployeePosMaster[], + ): OrgUpdatePayload[] { + const payloads: OrgUpdatePayload[] = []; + + // Process PosMaster records (PROFILE type) + for (const posMaster of posMasters) { + if (posMaster.current_holder && posMaster.current_holderId) { + payloads.push({ + profileId: posMaster.current_holderId, + rootDnaId: posMaster.orgRoot?.ancestorDNA || null, + child1DnaId: posMaster.orgChild1?.ancestorDNA || null, + child2DnaId: posMaster.orgChild2?.ancestorDNA || null, + child3DnaId: posMaster.orgChild3?.ancestorDNA || null, + child4DnaId: posMaster.orgChild4?.ancestorDNA || null, + profileType: "PROFILE", + }); + } + } + + // Process EmployeePosMaster records (PROFILE_EMPLOYEE type) + for (const employeePos of posMasterEmployee) { + if (employeePos.current_holder && employeePos.current_holderId) { + payloads.push({ + profileId: employeePos.current_holderId, + rootDnaId: employeePos.orgRoot?.ancestorDNA || null, + child1DnaId: employeePos.orgChild1?.ancestorDNA || null, + child2DnaId: employeePos.orgChild2?.ancestorDNA || null, + child3DnaId: employeePos.orgChild3?.ancestorDNA || null, + child4DnaId: employeePos.orgChild4?.ancestorDNA || null, + profileType: "PROFILE_EMPLOYEE", + }); + } + } + + return payloads; + } + + /** + * Group profile IDs by their type for separate Keycloak sync calls + */ + private groupProfileIdsByType(payloads: OrgUpdatePayload[]): Record { + const grouped: Record = { + PROFILE: [], + PROFILE_EMPLOYEE: [], + }; + + for (const payload of payloads) { + grouped[payload.profileType].push(payload.profileId); + } + + // Remove empty groups and deduplicate IDs within each group + const result: Record = {}; + for (const [type, ids] of Object.entries(grouped)) { + if (ids.length > 0) { + // Deduplicate while preserving order + result[type] = Array.from(new Set(ids)); + } + } + + return result; + } + + /** + * Split array into chunks of specified size + */ + private chunkArray(array: T[], chunkSize: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + return chunks; + } +} diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index a81e2af9..835e31f2 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -4,8 +4,8 @@ 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; const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET; -const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; -const API_KEY = process.env.API_KEY; +// const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; +// const API_KEY = process.env.API_KEY; let token: string | null = null; let decoded: DecodedJwt | null = null; @@ -165,16 +165,119 @@ export async function getUserList(first = "", max = "", search = "") { if (!res) return false; if (!res.ok) { - return Boolean(console.error("Keycloak Error Response: ", await res.json())); + const errorText = await res.text(); + return Boolean(console.error("Keycloak Error Response: ", errorText)); } - return ((await res.json()) as any[]).map((v: Record) => ({ + // Get raw text first to handle potential JSON parsing errors + const rawText = await res.text(); + + // Log response size for debugging + console.log(`[getUserList] Response size: ${rawText.length} bytes`); + + try { + const data = JSON.parse(rawText) as any[]; + return data.map((v: Record) => ({ + id: v.id, + username: v.username, + firstName: v.firstName, + lastName: v.lastName, + email: v.email, + enabled: v.enabled, + })); + } catch (error) { + console.error(`[getUserList] Failed to parse JSON response:`); + console.error(`[getUserList] Response preview (first 500 chars):`, rawText.substring(0, 500)); + console.error(`[getUserList] Response preview (last 200 chars):`, rawText.slice(-200)); + throw new Error( + `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, + ); + } +} + +/** + * Get all keycloak users with pagination to avoid response size limits + * + * Client must have permission to manage realm's user + * + * @returns user list if success, false otherwise. + */ +export async function getAllUsersPaginated( + search: string = "", + batchSize: number = 100, +): Promise< + | Array<{ + id: string; + username: string; + firstName?: string; + lastName?: string; + email?: string; + enabled: boolean; + }> + | false +> { + const allUsers: any[] = []; + let first = 0; + let hasMore = true; + + while (hasMore) { + const res = await fetch( + `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first}&max=${batchSize}${search ? `&search=${search}` : ""}`, + { + headers: { + authorization: `Bearer ${await getToken()}`, + "content-type": `application/json`, + }, + }, + ).catch((e) => console.log("Keycloak Error: ", e)); + + if (!res) return false; + if (!res.ok) { + const errorText = await res.text(); + console.error("Keycloak Error Response: ", errorText); + return false; + } + + const rawText = await res.text(); + + try { + const batch = JSON.parse(rawText) as any[]; + + if (batch.length === 0) { + hasMore = false; + } else { + allUsers.push(...batch); + first += batch.length; + hasMore = batch.length === batchSize; + + // Log progress for large datasets + if (allUsers.length % 500 === 0) { + console.log(`[getAllUsersPaginated] Fetched ${allUsers.length} users so far...`); + } + } + } catch (error) { + console.error(`[getAllUsersPaginated] Failed to parse JSON response at offset ${first}:`); + console.error( + `[getAllUsersPaginated] Response preview (first 500 chars):`, + rawText.substring(0, 500), + ); + console.error( + `[getAllUsersPaginated] Response preview (last 200 chars):`, + rawText.slice(-200), + ); + throw new Error(`Failed to parse Keycloak response as JSON at batch starting at ${first}.`); + } + } + + console.log(`[getAllUsersPaginated] Total users fetched: ${allUsers.length}`); + + return allUsers.map((v: any) => ({ id: v.id, username: v.username, firstName: v.firstName, lastName: v.lastName, email: v.email, - enabled: v.enabled, + enabled: v.enabled === true || v.enabled === "true", })); } @@ -220,17 +323,34 @@ export async function getUserListOrg(first = "", max = "", search = "", userIds: if (!res) return false; if (!res.ok) { - return Boolean(console.error("Keycloak Error Response: ", await res.json())); + const errorText = await res.text(); + return Boolean(console.error("Keycloak Error Response: ", errorText)); } - return ((await res.json()) as any[]).map((v: Record) => ({ - id: v.id, - username: v.username, - firstName: v.firstName, - lastName: v.lastName, - email: v.email, - enabled: v.enabled, - })); + // Get raw text first to handle potential JSON parsing errors + const rawText = await res.text(); + + try { + const data = JSON.parse(rawText) as any[]; + return data.map((v: Record) => ({ + id: v.id, + username: v.username, + firstName: v.firstName, + lastName: v.lastName, + email: v.email, + enabled: v.enabled, + })); + } catch (error) { + console.error(`[getUserListOrg] Failed to parse JSON response:`); + console.error( + `[getUserListOrg] Response preview (first 500 chars):`, + rawText.substring(0, 500), + ); + console.error(`[getUserListOrg] Response preview (last 200 chars):`, rawText.slice(-200)); + throw new Error( + `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, + ); + } } export async function getUserCountOrg(first = "", max = "", search = "", userIds: string[] = []) { @@ -444,10 +564,12 @@ export async function getRoles(name?: string, token?: string) { })); } - // return { - // id: data.id, - // name: data.name, - // }; + // Return single role object + return { + id: data.id, + name: data.name, + description: data.description, + }; } /** @@ -772,6 +894,73 @@ export async function changeUserPassword(userId: string, newPassword: string) { } } +/** + * Update user attributes in Keycloak + * + * @param userId - Keycloak user ID + * @param attributes - Object containing attribute names and their values (as arrays) + * @returns true if success, false otherwise + */ +export async function updateUserAttributes( + userId: string, + attributes: Record, +): Promise { + try { + // Get existing user data to preserve other attributes + const existingUser = await getUser(userId); + + if (!existingUser) { + console.error(`User ${userId} not found in Keycloak`); + return false; + } + + // Merge existing attributes with new attributes + // IMPORTANT: Spread all existing user fields to preserve firstName, lastName, email, etc. + // The Keycloak PUT endpoint performs a full update, so we must include all fields + const updatedAttributes = { + ...existingUser, + attributes: { + ...(existingUser.attributes || {}), + ...attributes, + }, + }; + + console.log( + `[updateUserAttributes] Sending to Keycloak:`, + JSON.stringify(updatedAttributes, null, 2), + ); + + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { + headers: { + authorization: `Bearer ${await getToken()}`, + "content-type": "application/json", + }, + method: "PUT", + body: JSON.stringify(updatedAttributes), + }).catch((e) => { + console.error(`[updateUserAttributes] Network error:`, e); + return null; + }); + + if (!res) { + console.error(`[updateUserAttributes] No response from Keycloak`); + return false; + } + + if (!res.ok) { + const errorText = await res.text(); + console.error(`[updateUserAttributes] Keycloak Error (${res.status}):`, errorText); + return false; + } + + console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`); + return true; + } catch (error) { + console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error); + return false; + } +} + // Function to reset password export async function resetPassword(username: string) { try { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index c27e6188..9a571572 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -75,6 +75,16 @@ export async function expressAuthentication( request.app.locals.logData.userName = payload.name; request.app.locals.logData.user = payload.preferred_username; + // เก็บค่า profileId และ orgRootDnaId จาก token (ใช้ค่าว่างถ้าไม่มี) + request.app.locals.logData.profileId = payload.profileId ?? ""; + request.app.locals.logData.orgRootDnaId = payload.orgRootDnaId ?? ""; + request.app.locals.logData.orgChild1DnaId = payload.orgChild1DnaId ?? ""; + request.app.locals.logData.orgChild2DnaId = payload.orgChild2DnaId ?? ""; + request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? ""; + request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? ""; + request.app.locals.logData.empType = payload.empType ?? ""; + request.app.locals.logData.prefix = payload.prefix ?? ""; + return payload; } diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index e5c48d9a..75c84d01 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -9,6 +9,14 @@ export type RequestWithUser = Request & { preferred_username: string; email: string; role: string[]; + profileId?: string; + prefix?: string; + orgRootDnaId?: string; + orgChild1DnaId?: string; + orgChild2DnaId?: string; + orgChild3DnaId?: string; + orgChild4DnaId?: string; + empType?: string; }; }; diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts new file mode 100644 index 00000000..5b61e286 --- /dev/null +++ b/src/services/KeycloakAttributeService.ts @@ -0,0 +1,928 @@ +import { AppDataSource } from "../database/data-source"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +// import { PosMaster } from "../entities/PosMaster"; +// import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +// import { OrgRoot } from "../entities/OrgRoot"; +import { + createUser, + getUser, + getUserByUsername, + updateUserAttributes, + deleteUser, + getRoles, + addUserRoles, + getAllUsersPaginated, +} from "../keycloak"; +import { OrgRevision } from "../entities/OrgRevision"; + +export interface UserProfileAttributes { + profileId: string | null; + orgRootDnaId: string | null; + orgChild1DnaId: string | null; + orgChild2DnaId: string | null; + orgChild3DnaId: string | null; + orgChild4DnaId: string | null; + empType: string | null; + prefix?: string | null; +} + +/** + * Keycloak Attribute Service + * Service for syncing profileId and orgRootDnaId to Keycloak user attributes + */ +export class KeycloakAttributeService { + private profileRepo = AppDataSource.getRepository(Profile); + private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); + // private posMasterRepo = AppDataSource.getRepository(PosMaster); + // private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); + // private orgRootRepo = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); + + /** + * Get profile attributes (profileId and orgRootDnaId) from database + * Searches in Profile table first (ข้าราชการ), then ProfileEmployee (ลูกจ้าง) + * + * @param keycloakUserId - Keycloak user ID + * @returns UserProfileAttributes with profileId and orgRootDnaId + */ + async getUserProfileAttributes(keycloakUserId: string): Promise { + // First, try to find in Profile (ข้าราชการ) + const revisionCurrent = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true }, + }); + const revisionId = revisionCurrent ? revisionCurrent.id : null; + const profileResult = await this.profileRepo + .createQueryBuilder("p") + .leftJoinAndSelect("p.current_holders", "pm") + .leftJoinAndSelect("pm.orgRoot", "orgRoot") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("p.keycloak = :keycloakUserId", { keycloakUserId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) + .getOne(); + + if ( + profileResult && + profileResult.current_holders && + profileResult.current_holders.length > 0 + ) { + const currentPos = profileResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: "OFFICER", + prefix: profileResult.prefix, + }; + } + + // If not found in Profile, try ProfileEmployee (ลูกจ้าง) + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("epm.orgChild1", "orgChild1") + .leftJoinAndSelect("epm.orgChild2", "orgChild2") + .leftJoinAndSelect("epm.orgChild3", "orgChild3") + .leftJoinAndSelect("epm.orgChild4", "orgChild4") + .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + + // Return null values if no profile found + return { + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + prefix: null, + }; + } + + /** + * Get profile attributes by profile ID directly + * Used for syncing specific profiles + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง + * @returns UserProfileAttributes with profileId and orgRootDnaId + */ + async getAttributesByProfileId( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise { + const revisionCurrent = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true }, + }); + const revisionId = revisionCurrent ? revisionCurrent.id : null; + if (profileType === "PROFILE") { + const profileResult = await this.profileRepo + .createQueryBuilder("p") + .leftJoinAndSelect("p.current_holders", "pm") + .leftJoinAndSelect("pm.orgRoot", "orgRoot") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("p.id = :profileId", { profileId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) + .getOne(); + + if ( + profileResult && + profileResult.current_holders && + profileResult.current_holders.length > 0 + ) { + const currentPos = profileResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: "OFFICER", + prefix: profileResult.prefix, + }; + } + } else { + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("pe.id = :profileId", { profileId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + } + + return { + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + prefix: null, + }; + } + + /** + * Sync user attributes to Keycloak + * + * @param keycloakUserId - Keycloak user ID + * @returns true if sync successful, false otherwise + */ + async syncUserAttributes(keycloakUserId: string): Promise { + try { + const attributes = await this.getUserProfileAttributes(keycloakUserId); + + if (!attributes.profileId) { + console.log(`No profile found for Keycloak user ${keycloakUserId}`); + return false; + } + + // Prepare attributes for Keycloak (must be arrays) + const keycloakAttributes: Record = { + profileId: [attributes.profileId], + orgRootDnaId: [attributes.orgRootDnaId || ""], + orgChild1DnaId: [attributes.orgChild1DnaId || ""], + orgChild2DnaId: [attributes.orgChild2DnaId || ""], + orgChild3DnaId: [attributes.orgChild3DnaId || ""], + orgChild4DnaId: [attributes.orgChild4DnaId || ""], + empType: [attributes.empType || ""], + prefix: [attributes.prefix || ""], + }; + + const success = await updateUserAttributes(keycloakUserId, keycloakAttributes); + + if (success) { + console.log(`Synced attributes for Keycloak user ${keycloakUserId}:`, attributes); + } + + return success; + } catch (error) { + console.error(`Error syncing attributes for Keycloak user ${keycloakUserId}:`, error); + return false; + } + } + + /** + * Sync attributes when organization changes + * This is called when a user moves to a different organization + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง + * @returns true if sync successful, false otherwise + */ + async syncOnOrganizationChange( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise { + try { + // Get the keycloak userId from the profile + let keycloakUserId: string | null = null; + + if (profileType === "PROFILE") { + const profile = await this.profileRepo.findOne({ where: { id: profileId } }); + keycloakUserId = profile?.keycloak || ""; + } else { + const profileEmployee = await this.profileEmployeeRepo.findOne({ + where: { id: profileId }, + }); + keycloakUserId = profileEmployee?.keycloak || ""; + } + + if (!keycloakUserId) { + console.log(`No Keycloak user ID found for profile ${profileId}`); + return false; + } + + return await this.syncUserAttributes(keycloakUserId); + } catch (error) { + console.error(`Error syncing organization change for profile ${profileId}:`, error); + return false; + } + } + + /** + * Batch sync multiple users with unlimited count and parallel processing + * Useful for initial sync or periodic updates + * + * @param options - Optional configuration (limit for testing, concurrency for parallel processing) + * @returns Object with success count and details + */ + async batchSyncUsers(options?: { + limit?: number; + concurrency?: number; + }): Promise<{ total: number; success: number; failed: number; details: any[] }> { + const limit = options?.limit; + const concurrency = options?.concurrency ?? 5; + + const result = { + total: 0, + success: 0, + failed: 0, + details: [] as any[], + }; + + try { + // Build query for profiles with keycloak IDs (ข้าราชการ) + const profileQuery = this.profileRepo + .createQueryBuilder("p") + .where("p.keycloak IS NOT NULL") + .andWhere("p.keycloak != :empty", { empty: "" }); + + // Build query for profileEmployees with keycloak IDs (ลูกจ้าง) + const profileEmployeeQuery = this.profileEmployeeRepo + .createQueryBuilder("pe") + .where("pe.keycloak IS NOT NULL") + .andWhere("pe.keycloak != :empty", { empty: "" }); + + // Apply limit if specified (for testing purposes) + if (limit !== undefined) { + profileQuery.take(limit); + profileEmployeeQuery.take(limit); + } + + // Get profiles from both tables + const [profiles, profileEmployees] = await Promise.all([ + profileQuery.getMany(), + profileEmployeeQuery.getMany(), + ]); + + const allProfiles = [ + ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), + ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), + ]; + + result.total = allProfiles.length; + + // Process in parallel with concurrency limit + const processedResults = await this.processInParallel( + allProfiles, + concurrency, + async ({ profile, type }, _index) => { + const keycloakUserId = profile.keycloak; + + try { + const success = await this.syncOnOrganizationChange(profile.id, type); + if (success) { + result.success++; + return { + profileId: profile.id, + keycloakUserId, + status: "success", + }; + } else { + result.failed++; + return { + profileId: profile.id, + keycloakUserId, + status: "failed", + error: "Sync returned false", + }; + } + } catch (error: any) { + result.failed++; + return { + profileId: profile.id, + keycloakUserId, + status: "error", + error: error.message, + }; + } + }, + ); + + // Separate results from errors + for (const resultItem of processedResults) { + if ("error" in resultItem) { + result.failed++; + result.details.push({ + profileId: "unknown", + keycloakUserId: "unknown", + status: "error", + error: JSON.stringify(resultItem.error), + }); + } else { + result.details.push(resultItem); + } + } + + console.log( + `Batch sync completed: total=${result.total}, success=${result.success}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in batch sync:", error); + } + + return result; + } + + /** + * Get current Keycloak attributes for a user + * + * @param keycloakUserId - Keycloak user ID + * @returns Current attributes from Keycloak + */ + async getCurrentKeycloakAttributes( + keycloakUserId: string, + ): Promise { + try { + const user = await getUser(keycloakUserId); + + if (!user || !user.attributes) { + return null; + } + + return { + profileId: user.attributes.profileId?.[0] || "", + orgRootDnaId: user.attributes.orgRootDnaId?.[0] || "", + orgChild1DnaId: user.attributes.orgChild1DnaId?.[0] || "", + orgChild2DnaId: user.attributes.orgChild2DnaId?.[0] || "", + orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "", + orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "", + empType: user.attributes.empType?.[0] || "", + prefix: user.attributes.prefix?.[0] || "", + }; + } catch (error) { + console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error); + return null; + } + } + + /** + * Ensure Keycloak user exists for a profile + * Creates user if keycloak field is empty OR if stored keycloak ID doesn't exist in Keycloak + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' + * @returns Object with status and details + */ + async ensureKeycloakUser( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise<{ + success: boolean; + action: "created" | "verified" | "skipped" | "error"; + keycloakUserId?: string; + error?: string; + }> { + try { + // Get profile from database + let profile: Profile | ProfileEmployee | null = null; + + if (profileType === "PROFILE") { + profile = await this.profileRepo.findOne({ where: { id: profileId } }); + } else { + profile = await this.profileEmployeeRepo.findOne({ where: { id: profileId } }); + } + + if (!profile) { + return { + success: false, + action: "error", + error: `Profile ${profileId} not found in database`, + }; + } + + // Check if citizenId exists + if (!profile.citizenId) { + return { + success: false, + action: "skipped", + error: "No citizenId found", + }; + } + + // Case 1: keycloak field is empty -> create new user + if (!profile.keycloak || profile.keycloak.trim() === "") { + const result = await this.createKeycloakUserFromProfile(profile, profileType); + return result; + } + + // Case 2: keycloak field is not empty -> verify user exists in Keycloak + const existingUser = await getUser(profile.keycloak); + + if (!existingUser) { + // User doesn't exist in Keycloak, create new one + console.log( + `Keycloak user ${profile.keycloak} not found in Keycloak, creating new user for profile ${profileId}`, + ); + const result = await this.createKeycloakUserFromProfile(profile, profileType); + return result; + } + + // User exists in Keycloak, verified + return { + success: true, + action: "verified", + keycloakUserId: profile.keycloak, + }; + } catch (error: any) { + console.error(`Error ensuring Keycloak user for profile ${profileId}:`, error); + return { + success: false, + action: "error", + error: error.message || "Unknown error", + }; + } + } + + /** + * Create Keycloak user from profile data + * + * @param profile - Profile or ProfileEmployee entity + * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' + * @returns Object with status and details + */ + private async createKeycloakUserFromProfile( + profile: Profile | ProfileEmployee, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise<{ + success: boolean; + action: "created" | "verified" | "skipped" | "error"; + keycloakUserId?: string; + error?: string; + }> { + try { + // Check if user already exists by username (citizenId) + const existingUserByUsername = await getUserByUsername(profile.citizenId); + if (Array.isArray(existingUserByUsername) && existingUserByUsername.length > 0) { + // User already exists with this username, update the keycloak field + const existingUserId = existingUserByUsername[0].id; + console.log( + `User with citizenId ${profile.citizenId} already exists in Keycloak with ID ${existingUserId}`, + ); + + // Update the keycloak field in database + if (profileType === "PROFILE") { + await this.profileRepo.update(profile.id, { keycloak: existingUserId }); + } else { + await this.profileEmployeeRepo.update(profile.id, { keycloak: existingUserId }); + } + + // Assign default USER role to existing user + const userRole = await getRoles("USER"); + if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { + const roleAssigned = await addUserRoles(existingUserId, [ + { id: String(userRole.id), name: String(userRole.name) }, + ]); + if (roleAssigned) { + console.log(`Assigned USER role to existing user ${existingUserId}`); + } else { + console.warn(`Failed to assign USER role to existing user ${existingUserId}`); + } + } else { + console.warn(`USER role not found in Keycloak`); + } + + return { + success: true, + action: "verified", + keycloakUserId: existingUserId, + }; + } + + // Create new user in Keycloak + const createResult = await createUser(profile.citizenId, "P@ssw0rd", { + firstName: profile.firstName || "", + lastName: profile.lastName || "", + email: profile.email || undefined, + enabled: true, + }); + + if (!createResult || typeof createResult !== "string") { + return { + success: false, + action: "error", + error: "Failed to create user in Keycloak", + }; + } + + const keycloakUserId = createResult; + + // Update the keycloak field in database + if (profileType === "PROFILE") { + await this.profileRepo.update(profile.id, { keycloak: keycloakUserId }); + } else { + await this.profileEmployeeRepo.update(profile.id, { keycloak: keycloakUserId }); + } + + // Assign default USER role + const userRole = await getRoles("USER"); + if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { + const roleAssigned = await addUserRoles(keycloakUserId, [ + { id: String(userRole.id), name: String(userRole.name) }, + ]); + if (roleAssigned) { + console.log(`Assigned USER role to user ${keycloakUserId}`); + } else { + console.warn(`Failed to assign USER role to user ${keycloakUserId}`); + } + } else { + console.warn(`USER role not found in Keycloak`); + } + + console.log( + `Created Keycloak user for profile ${profile.id} (citizenId: ${profile.citizenId}) with ID ${keycloakUserId}`, + ); + + return { + success: true, + action: "created", + keycloakUserId, + }; + } catch (error: any) { + console.error(`Error creating Keycloak user for profile ${profile.id}:`, error); + return { + success: false, + action: "error", + error: error.message || "Unknown error", + }; + } + } + + /** + * Process items in parallel with concurrency limit + */ + private async processInParallel( + items: T[], + concurrencyLimit: number, + processor: (item: T, index: number) => Promise, + ): Promise> { + const results: Array = []; + + // Process items in batches + for (let i = 0; i < items.length; i += concurrencyLimit) { + const batch = items.slice(i, i + concurrencyLimit); + + // Process batch in parallel with error handling + const batchResults = await Promise.all( + batch.map(async (item, batchIndex) => { + try { + return await processor(item, i + batchIndex); + } catch (error) { + return { error }; + } + }), + ); + + results.push(...batchResults); + + // Log progress after each batch + const completed = Math.min(i + concurrencyLimit, items.length); + console.log(`Progress: ${completed}/${items.length}`); + } + + return results; + } + + /** + * Batch ensure Keycloak users for all profiles + * Processes all rows in Profile and ProfileEmployee tables + * + * @returns Object with total, success, failed counts and details + */ + async batchEnsureKeycloakUsers(): Promise<{ + total: number; + created: number; + verified: number; + skipped: number; + failed: number; + details: Array<{ + profileId: string; + profileType: string; + action: string; + keycloakUserId?: string; + error?: string; + }>; + }> { + const result = { + total: 0, + created: 0, + verified: 0, + skipped: 0, + failed: 0, + details: [] as Array<{ + profileId: string; + profileType: string; + action: string; + keycloakUserId?: string; + error?: string; + }>, + }; + + try { + // Get all profiles from Profile table (ข้าราชการ) + const profiles = await this.profileRepo.find({ where: { isLeave: false } }); // Only active profiles + + // Get all profiles from ProfileEmployee table (ลูกจ้าง) + const profileEmployees = await this.profileEmployeeRepo.find({ where: { isLeave: false } }); // Only active profiles + + const allProfiles = [ + ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), + ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), + ]; + + result.total = allProfiles.length; + + // Process in parallel with concurrency limit + const CONCURRENCY_LIMIT = 5; // Adjust based on environment + + const processedResults = await this.processInParallel( + allProfiles, + CONCURRENCY_LIMIT, + async ({ profile, type }) => { + const ensureResult = await this.ensureKeycloakUser(profile.id, type); + + // Update counters + switch (ensureResult.action) { + case "created": + result.created++; + break; + case "verified": + result.verified++; + break; + case "skipped": + result.skipped++; + break; + case "error": + result.failed++; + break; + } + + return { + profileId: profile.id, + profileType: type, + action: ensureResult.action, + keycloakUserId: ensureResult.keycloakUserId, + error: ensureResult.error, + }; + }, + ); + + // Separate results from errors + for (const resultItem of processedResults) { + if ("error" in resultItem) { + result.failed++; + result.details.push({ + profileId: "unknown", + profileType: "unknown", + action: "error", + error: JSON.stringify(resultItem.error), + }); + } else { + result.details.push(resultItem); + } + } + + console.log( + `Batch ensure Keycloak users completed: total=${result.total}, created=${result.created}, verified=${result.verified}, skipped=${result.skipped}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in batch ensure Keycloak users:", error); + } + + return result; + } + + /** + * Clear orphaned Keycloak users + * Deletes users in Keycloak that are not referenced in Profile or ProfileEmployee tables + * + * @param skipUsernames - Array of usernames to skip (e.g., ['super_admin']) + * @returns Object with counts and details + */ + async clearOrphanedKeycloakUsers(skipUsernames: string[] = []): Promise<{ + totalInKeycloak: number; + totalInDatabase: number; + orphanedCount: number; + deleted: number; + skipped: number; + failed: number; + details: Array<{ + keycloakUserId: string; + username: string; + action: "deleted" | "skipped" | "error"; + error?: string; + }>; + }> { + const result = { + totalInKeycloak: 0, + totalInDatabase: 0, + orphanedCount: 0, + deleted: 0, + skipped: 0, + failed: 0, + details: [] as Array<{ + keycloakUserId: string; + username: string; + action: "deleted" | "skipped" | "error"; + error?: string; + }>, + }; + + try { + // Get all keycloak IDs from database (Profile + ProfileEmployee) + const profiles = await this.profileRepo + .createQueryBuilder("p") + .where("p.keycloak IS NOT NULL") + .andWhere("p.keycloak != :empty", { empty: "" }) + .getMany(); + + const profileEmployees = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .where("pe.keycloak IS NOT NULL") + .andWhere("pe.keycloak != :empty", { empty: "" }) + .getMany(); + + // Create a Set of all keycloak IDs in database for O(1) lookup + const databaseKeycloakIds = new Set(); + for (const p of profiles) { + if (p.keycloak) databaseKeycloakIds.add(p.keycloak); + } + for (const pe of profileEmployees) { + if (pe.keycloak) databaseKeycloakIds.add(pe.keycloak); + } + + result.totalInDatabase = databaseKeycloakIds.size; + + // Get all users from Keycloak with pagination to avoid response size limits + const keycloakUsers = await getAllUsersPaginated(); + if (!keycloakUsers || typeof keycloakUsers !== "object") { + throw new Error("Failed to get users from Keycloak"); + } + + result.totalInKeycloak = keycloakUsers.length; + + // Find orphaned users (in Keycloak but not in database) + const orphanedUsers = keycloakUsers.filter((user: any) => !databaseKeycloakIds.has(user.id)); + result.orphanedCount = orphanedUsers.length; + + // Delete orphaned users (skip protected ones) + for (const user of orphanedUsers) { + const username = user.username; + const userId = user.id; + + // Check if user should be skipped + if (skipUsernames.includes(username)) { + result.skipped++; + result.details.push({ + keycloakUserId: userId, + username, + action: "skipped", + }); + continue; + } + + // Delete user from Keycloak + try { + const deleteSuccess = await deleteUser(userId); + if (deleteSuccess) { + result.deleted++; + result.details.push({ + keycloakUserId: userId, + username, + action: "deleted", + }); + } else { + result.failed++; + result.details.push({ + keycloakUserId: userId, + username, + action: "error", + error: "Failed to delete user from Keycloak", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ + keycloakUserId: userId, + username, + action: "error", + error: error.message || "Unknown error", + }); + } + } + + console.log( + `Clear orphaned Keycloak users completed: totalInKeycloak=${result.totalInKeycloak}, totalInDatabase=${result.totalInDatabase}, orphaned=${result.orphanedCount}, deleted=${result.deleted}, skipped=${result.skipped}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in clear orphaned Keycloak users:", error); + throw error; + } + + return result; + } +} From c5c19b6d5ef65aa969196b735bff0759ab5cf323 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 26 Feb 2026 23:16:43 +0700 Subject: [PATCH 126/178] Revert "fix: sync and script keycloak" This reverts commit d667ad9173d13cc83b67e1f23b6f420499c5ce76. --- scripts/KEYCLOAK_SYNC_README.md | 149 --- scripts/assign-user-role.ts | 269 ----- scripts/clear-orphaned-users.ts | 80 -- scripts/ensure-users.ts | 91 -- scripts/sync-all.ts | 93 -- src/app.ts | 44 +- src/controllers/KeycloakSyncController.ts | 254 ----- src/controllers/ScriptProfileOrgController.ts | 326 ------ src/keycloak/index.ts | 225 +---- src/middlewares/auth.ts | 10 - src/middlewares/user.ts | 8 - src/services/KeycloakAttributeService.ts | 928 ------------------ 12 files changed, 33 insertions(+), 2444 deletions(-) delete mode 100644 scripts/KEYCLOAK_SYNC_README.md delete mode 100644 scripts/assign-user-role.ts delete mode 100644 scripts/clear-orphaned-users.ts delete mode 100644 scripts/ensure-users.ts delete mode 100644 scripts/sync-all.ts delete mode 100644 src/controllers/KeycloakSyncController.ts delete mode 100644 src/controllers/ScriptProfileOrgController.ts delete mode 100644 src/services/KeycloakAttributeService.ts diff --git a/scripts/KEYCLOAK_SYNC_README.md b/scripts/KEYCLOAK_SYNC_README.md deleted file mode 100644 index 4c61ec75..00000000 --- a/scripts/KEYCLOAK_SYNC_README.md +++ /dev/null @@ -1,149 +0,0 @@ -# Keycloak Sync Scripts - -This directory contains standalone scripts for managing Keycloak users from the CLI. These scripts are useful for maintenance, setup, and troubleshooting Keycloak synchronization. - -## Prerequisites - -- Node.js and TypeScript installed -- Database connection configured in `.env` -- Keycloak connection configured in `.env` -- Run with `ts-node` or compile first - -## Environment Variables - -Ensure these are set in your `.env` file: - -```bash -# Database -DB_HOST=localhost -DB_PORT=3306 -DB_NAME=your_database -DB_USER=your_user -DB_PASSWORD=your_password - -# Keycloak -KC_URL=https://your-keycloak-url -KC_REALMS=your-realm -KC_SERVICE_ACCOUNT_CLIENT_ID=your-client-id -KC_SERVICE_ACCOUNT_SECRET=your-client-secret -``` - -## Scripts - -### 1. clear-orphaned-users.ts - -Deletes Keycloak users that don't exist in the database (Profile + ProfileEmployee tables). Useful for cleaning up invalid users. - -**Protected usernames** (never deleted): `super_admin`, `admin_issue` - -```bash -# Dry run (preview changes) -ts-node scripts/clear-orphaned-users.ts --dry-run - -# Live execution -ts-node scripts/clear-orphaned-users.ts -``` - -**Output:** -- Total users in Keycloak -- Total users in Database -- Orphaned users found -- Deleted/Skipped/Failed counts - -### 2. ensure-users.ts - -Checks and creates Keycloak users for all profiles. Includes role assignment (USER role) automatically. - -```bash -# Dry run (preview changes) -ts-node scripts/ensure-users.ts --dry-run - -# Live execution -ts-node scripts/ensure-users.ts - -# Test with limited profiles (for testing) -ts-node scripts/ensure-users.ts --dry-run --limit=10 -``` - -**Output:** -- Total profiles processed -- Created users (new Keycloak accounts) -- Verified users (already exists) -- Skipped profiles (no citizenId) -- Failed count - -### 3. sync-all.ts - -Syncs all attributes to Keycloak for users with existing Keycloak IDs. - -**Attributes synced:** -- `profileId` -- `orgRootDnaId` -- `orgChild1DnaId` -- `orgChild2DnaId` -- `orgChild3DnaId` -- `orgChild4DnaId` -- `empType` -- `prefix` - -```bash -# Dry run (preview changes) -ts-node scripts/sync-all.ts --dry-run - -# Live execution -ts-node scripts/sync-all.ts - -# Test with limited profiles -ts-node scripts/sync-all.ts --dry-run --limit=10 - -# Adjust concurrency (default: 5) -ts-node scripts/sync-all.ts --concurrency=10 -``` - -**Output:** -- Total profiles with Keycloak IDs -- Success/Failed counts -- Error details for failures - -## Recommended Execution Order - -For initial setup or full resynchronization: - -1. **clear-orphaned-users** - Clean up invalid users first - ```bash - npx ts-node scripts/clear-orphaned-users.ts - ``` - -2. **ensure-users** - Create missing users and assign roles - ```bash - npx ts-node scripts/ensure-users.ts - ``` - -3. **sync-all** - Sync all attributes to Keycloak - ```bash - npx ts-node scripts/sync-all.ts - ``` - -## Tips - -- Always run with `--dry-run` first to preview changes -- Use `--limit=N` for testing before running on full dataset -- Scripts process both Profile (ข้าราชการ) and ProfileEmployee (ลูกจ้าง) tables -- Only active profiles (`isLeave: false`) are processed by ensure-users -- The `USER` role is automatically assigned to new/verified users - -## Troubleshooting - -**Database connection error:** -- Check `.env` database variables -- Ensure database server is running - -**Keycloak connection error:** -- Check `KC_URL`, `KC_REALMS` in `.env` -- Verify service account credentials -- Check network connectivity to Keycloak - -**USER role not found:** -- Log in to Keycloak admin console -- Create a `USER` role in your realm -- Ensure service account has `manage-users` and `view-users` permissions \ No newline at end of file diff --git a/scripts/assign-user-role.ts b/scripts/assign-user-role.ts deleted file mode 100644 index 4161b31b..00000000 --- a/scripts/assign-user-role.ts +++ /dev/null @@ -1,269 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { Profile } from "../src/entities/Profile"; -import { ProfileEmployee } from "../src/entities/ProfileEmployee"; -import * as keycloak from "../src/keycloak/index"; - -const USER_ROLE_NAME = "USER"; - -interface AssignOptions { - dryRun: boolean; - targetUsernames?: string[]; -} - -interface UserWithKeycloak { - keycloakId: string; - citizenId: string; - source: "Profile" | "ProfileEmployee"; -} - -interface AssignResult { - total: number; - assigned: number; - skipped: number; - failed: number; - errors: Array<{ - userId: string; - username: string; - error: string; - }>; -} - -/** - * Get all users from database who have Keycloak IDs set - */ -async function getUsersWithKeycloak(): Promise { - const users: UserWithKeycloak[] = []; - const profileRepo = AppDataSource.getRepository(Profile); - const profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); - - // Get from Profile table - const profiles = await profileRepo - .createQueryBuilder("profile") - .where("profile.keycloak IS NOT NULL") - .andWhere("profile.keycloak != ''") - .getMany(); - - for (const profile of profiles) { - users.push({ - keycloakId: profile.keycloak, - citizenId: profile.citizenId || profile.id, - source: "Profile", - }); - } - - // Get from ProfileEmployee table - const employees = await profileEmployeeRepo - .createQueryBuilder("profileEmployee") - .where("profileEmployee.keycloak IS NOT NULL") - .andWhere("profileEmployee.keycloak != ''") - .getMany(); - - for (const employee of employees) { - // Avoid duplicates - check if keycloak ID already exists - if (!users.some((u) => u.keycloakId === employee.keycloak)) { - users.push({ - keycloakId: employee.keycloak, - citizenId: employee.citizenId || employee.id, - source: "ProfileEmployee", - }); - } - } - - return users; -} - -/** - * Assign USER role to users who don't have it - */ -async function assignUserRoleToUsers( - users: UserWithKeycloak[], - userRoleId: string, - options: AssignOptions, -): Promise { - const result: AssignResult = { - total: users.length, - assigned: 0, - skipped: 0, - failed: 0, - errors: [], - }; - - console.log(`Processing ${result.total} users...`); - - for (let i = 0; i < users.length; i++) { - const user = users[i]; - const index = i + 1; - - try { - // Get user's current roles - const userRoles = await keycloak.getUserRoles(user.keycloakId); - - if (!userRoles || typeof userRoles === "boolean") { - console.log( - `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Failed to get roles`, - ); - result.failed++; - result.errors.push({ - userId: user.keycloakId, - username: user.citizenId, - error: "Failed to get user roles", - }); - continue; - } - - // Handle both array and single object return types - // getUserRoles can return an array or a single object - const rolesArray = Array.isArray(userRoles) ? userRoles : [userRoles]; - - // Check if user already has USER role - const hasUserRole = rolesArray.some( - (role: { id: string; name: string }) => role.name === USER_ROLE_NAME, - ); - - if (hasUserRole) { - console.log( - `[${index}/${result.total}] Skipped: ${user.citizenId} (source: ${user.source}) - Already has USER role`, - ); - result.skipped++; - continue; - } - - // Assign USER role - if (options.dryRun) { - console.log( - `[${index}/${result.total}] [DRY-RUN] Would assign USER role: ${user.citizenId} (source: ${user.source})`, - ); - result.assigned++; - } else { - const assignResult = await keycloak.addUserRoles(user.keycloakId, [ - { id: userRoleId, name: USER_ROLE_NAME }, - ]); - - if (assignResult) { - console.log( - `[${index}/${result.total}] Assigned USER role: ${user.citizenId} (source: ${user.source})`, - ); - result.assigned++; - } else { - console.log( - `[${index}/${result.total}] Failed: ${user.citizenId} (source: ${user.source}) - Could not assign role`, - ); - result.failed++; - result.errors.push({ - userId: user.keycloakId, - username: user.citizenId, - error: "Failed to assign USER role", - }); - } - } - - // Small delay to avoid rate limiting - if (index % 50 === 0) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.log( - `[${index}/${result.total}] Error: ${user.citizenId} (source: ${user.source}) - ${errorMessage}`, - ); - result.failed++; - result.errors.push({ - userId: user.keycloakId, - username: user.citizenId, - error: errorMessage, - }); - } - } - - return result; -} - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - - console.log("=".repeat(60)); - console.log("Keycloak USER Role Assignment Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - // Get USER role from Keycloak - console.log(""); - const userRole = await keycloak.getRoles(USER_ROLE_NAME); - - // Check if USER role exists and is valid (has id property) - if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { - console.error(`ERROR: ${USER_ROLE_NAME} role not found in Keycloak`); - await AppDataSource.destroy(); - process.exit(1); - } - - // Type assertion via unknown to bypass union type issues - const role = userRole as unknown as { id: string; name: string; description?: string }; - const roleId = role.id; - console.log(`${USER_ROLE_NAME} role found: ${roleId}`); - console.log(""); - - // Get users from database - console.log("Fetching users from database..."); - let users = await getUsersWithKeycloak(); - - if (users.length === 0) { - console.log("No users with Keycloak IDs found in database"); - await AppDataSource.destroy(); - process.exit(0); - } - - console.log(`Found ${users.length} users with Keycloak IDs`); - console.log(""); - - // Assign USER role - const result = await assignUserRoleToUsers(users, roleId, { dryRun }); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total users: ${result.total}`); - console.log(` Assigned: ${result.assigned}`); - console.log(` Skipped: ${result.skipped}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.errors.length > 0) { - console.log(""); - console.log("Errors:"); - result.errors.forEach((e) => { - console.log(` ${e.username}: ${e.error}`); - }); - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/scripts/clear-orphaned-users.ts b/scripts/clear-orphaned-users.ts deleted file mode 100644 index 67eee99d..00000000 --- a/scripts/clear-orphaned-users.ts +++ /dev/null @@ -1,80 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; -import * as keycloak from "../src/keycloak/index"; - -const PROTECTED_USERNAMES = ["super_admin", "admin_issue"]; - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - const skipUsernames = [...PROTECTED_USERNAMES]; - - console.log("=".repeat(60)); - console.log("Clear Orphaned Keycloak Users Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - console.log(`Protected usernames: ${skipUsernames.join(", ")}`); - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log(""); - - // Run the orphaned user cleanup - const service = new KeycloakAttributeService(); - const result = await service.clearOrphanedKeycloakUsers(skipUsernames); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total users in Keycloak: ${result.totalInKeycloak}`); - console.log(` Total users in Database: ${result.totalInDatabase}`); - console.log(` Orphaned users: ${result.orphanedCount}`); - console.log(` Deleted: ${result.deleted}`); - console.log(` Skipped: ${result.skipped}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.details.length > 0) { - console.log(""); - console.log("Details:"); - for (const detail of result.details) { - const status = - detail.action === "deleted" - ? "[DELETED]" - : detail.action === "skipped" - ? "[SKIPPED]" - : "[ERROR]"; - console.log( - ` ${status} ${detail.username} (${detail.keycloakUserId})${detail.error ? ": " + detail.error : ""}`, - ); - } - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/scripts/ensure-users.ts b/scripts/ensure-users.ts deleted file mode 100644 index 14522d96..00000000 --- a/scripts/ensure-users.ts +++ /dev/null @@ -1,91 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; -import * as keycloak from "../src/keycloak/index"; - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - const limitArg = args.find((arg) => arg.startsWith("--limit=")); - const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; - - console.log("=".repeat(60)); - console.log("Ensure Keycloak Users Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - if (limit !== undefined) { - console.log(`Limit: ${limit} profiles per table (for testing)`); - } - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log(""); - - // Verify USER role exists - console.log("Verifying USER role in Keycloak..."); - const userRole = await keycloak.getRoles("USER"); - - if (!userRole || typeof userRole === "boolean" || userRole === null || !("id" in userRole)) { - console.error("ERROR: USER role not found in Keycloak"); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log("USER role found"); - console.log(""); - - // Run the ensure users operation - const service = new KeycloakAttributeService(); - console.log("Ensuring Keycloak users for all profiles..."); - console.log(""); - - const result = await service.batchEnsureKeycloakUsers(); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total profiles: ${result.total}`); - console.log(` Created: ${result.created}`); - console.log(` Verified: ${result.verified}`); - console.log(` Skipped: ${result.skipped}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.failed > 0) { - console.log(""); - console.log("Failed Details:"); - const failedDetails = result.details.filter((d) => d.action === "error" || !!d.error); - for (const detail of failedDetails) { - console.log( - ` [${detail.profileType}] ${detail.profileId}: ${detail.error || "Unknown error"}`, - ); - } - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/scripts/sync-all.ts b/scripts/sync-all.ts deleted file mode 100644 index 9090dd17..00000000 --- a/scripts/sync-all.ts +++ /dev/null @@ -1,93 +0,0 @@ -import "dotenv/config"; -import { AppDataSource } from "../src/database/data-source"; -import { KeycloakAttributeService } from "../src/services/KeycloakAttributeService"; -import * as keycloak from "../src/keycloak/index"; - -/** - * Main function - */ -async function main() { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - const limitArg = args.find((arg) => arg.startsWith("--limit=")); - const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : undefined; - const concurrencyArg = args.find((arg) => arg.startsWith("--concurrency=")); - const concurrency = concurrencyArg ? parseInt(concurrencyArg.split("=")[1], 10) : undefined; - - console.log("=".repeat(60)); - console.log("Sync All Attributes to Keycloak Script"); - console.log("=".repeat(60)); - console.log(`Mode: ${dryRun ? "DRY-RUN (no changes)" : "LIVE"}`); - if (limit !== undefined) { - console.log(`Limit: ${limit} profiles per table (for testing)`); - } - if (concurrency !== undefined) { - console.log(`Concurrency: ${concurrency}`); - } - console.log(""); - - console.log("Attributes to sync:"); - console.log(" - profileId"); - console.log(" - orgRootDnaId"); - console.log(" - orgChild1DnaId"); - console.log(" - orgChild2DnaId"); - console.log(" - orgChild3DnaId"); - console.log(" - orgChild4DnaId"); - console.log(" - empType"); - console.log(" - prefix"); - console.log(""); - - // Initialize database - try { - await AppDataSource.initialize(); - console.log("Database connected"); - } catch (error) { - console.error("Failed to connect to database:", error); - process.exit(1); - } - - // Validate Keycloak connection - try { - await keycloak.getToken(); - console.log("Keycloak connected"); - } catch (error) { - console.error("Failed to connect to Keycloak:", error); - await AppDataSource.destroy(); - process.exit(1); - } - - console.log(""); - - // Run the sync operation - const service = new KeycloakAttributeService(); - console.log("Syncing attributes for all profiles with Keycloak IDs..."); - console.log(""); - - const result = await service.batchSyncUsers({ limit, concurrency }); - - // Summary - console.log(""); - console.log("=".repeat(60)); - console.log("Summary:"); - console.log(` Total profiles: ${result.total}`); - console.log(` Success: ${result.success}`); - console.log(` Failed: ${result.failed}`); - console.log("=".repeat(60)); - - if (result.failed > 0) { - console.log(""); - console.log("Failed Details:"); - for (const detail of result.details.filter( - (d) => d.status === "failed" || d.status === "error", - )) { - console.log( - ` ${detail.profileId} (${detail.keycloakUserId}): ${detail.error || "Sync failed"}`, - ); - } - } - - // Cleanup - await AppDataSource.destroy(); -} - -main().catch(console.error); diff --git a/src/app.ts b/src/app.ts index 29d50749..75d0bfea 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,7 +18,6 @@ import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; -import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; async function main() { await AppDataSource.initialize(); @@ -53,19 +52,8 @@ async function main() { const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_PORT = +(process.env.APP_PORT || 3000); - // Cron job for executing command - every day at 00:30:00 - const cronTime_command = "0 30 0 * * *"; - cron.schedule(cronTime_command, async () => { - try { - const commandController = new CommandController(); - await commandController.cronjobCommand(); - } catch (error) { - console.error("Error executing function from controller:", error); - } - }); - - // Cron job for updating org revision - every day at 01:00:00 - const cronTime = "0 0 1 * * *"; + const cronTime = "0 0 3 * * *"; // ตั้งเวลาทุกวันเวลา 03:00:00 + // const cronTime = "*/10 * * * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); @@ -75,8 +63,18 @@ async function main() { } }); - // Cron job for updating retirement status - every day at 02:00:00 on the 1st of October - const cronTime_Oct = "0 0 2 10 *"; + const cronTime_command = "0 0 2 * * *"; + // const cronTime_command = "*/10 * * * * *"; + cron.schedule(cronTime_command, async () => { + try { + const commandController = new CommandController(); + await commandController.cronjobCommand(); + } catch (error) { + console.error("Error executing function from controller:", error); + } + }); + + const cronTime_Oct = "0 0 1 10 *"; cron.schedule(cronTime_Oct, async () => { try { const commandController = new CommandController(); @@ -86,19 +84,7 @@ async function main() { } }); - // Cron job for updating org DNA - every day at 03:00:00 - const cronTime_UpdateOrg = "0 0 3 * * *"; - cron.schedule(cronTime_UpdateOrg, async () => { - try { - const scriptProfileOrgController = new ScriptProfileOrgController(); - await scriptProfileOrgController.cronjobUpdateOrg({} as any); - } catch (error) { - console.error("Error executing cronjobUpdateOrg:", error); - } - }); - - // Cron job for updating tenure - every day at 04:00:00 - const cronTime_Tenure = "0 0 4 * * *"; + const cronTime_Tenure = "0 0 0 * * *"; cron.schedule(cronTime_Tenure, async () => { try { const profileSalaryController = new ProfileSalaryController(); diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts deleted file mode 100644 index 5f814238..00000000 --- a/src/controllers/KeycloakSyncController.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - Controller, - Post, - Get, - Route, - Security, - Tags, - Path, - Request, - Response, - Query, - Body, -} from "tsoa"; -import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; -import HttpSuccess from "../interfaces/http-success"; -import HttpStatus from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { RequestWithUser } from "../middlewares/user"; - -@Route("api/v1/org/keycloak-sync") -@Tags("Keycloak Sync") -@Security("bearerAuth") -@Response( - HttpStatus.INTERNAL_SERVER_ERROR, - "เกิดข้อผิดพลาด ไม่สามารถดำเนินการได้ กรุณาลองใหม่ในภายหลัง", -) -export class KeycloakSyncController extends Controller { - private keycloakAttributeService = new KeycloakAttributeService(); - - /** - * Sync attributes for the current logged-in user - * - * @summary Sync profileId and rootDnaId to Keycloak for current user - */ - @Post("sync-me") - async syncCurrentUser(@Request() request: RequestWithUser) { - const keycloakUserId = request.user.sub; - - if (!keycloakUserId) { - throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); - } - - // Get attributes from database before sync - const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); - - const success = await this.keycloakAttributeService.syncUserAttributes(keycloakUserId); - - if (!success) { - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ กรุณาติดต่อผู้ดูแลระบบ", - ); - } - - // Verify sync by fetching attributes from Keycloak after update - const kcAttrsAfter = - await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); - - return new HttpSuccess({ - message: "Sync ข้อมูลสำเร็จ", - syncedToKeycloak: !!kcAttrsAfter?.profileId, - databaseAttributes: dbAttrs, - keycloakAttributesAfter: kcAttrsAfter, - }); - } - - /** - * Get current attributes of the logged-in user - * - * @summary Get current profileId and rootDnaId from Keycloak - */ - // @Get("my-attributes") - async getMyAttributes(@Request() request: RequestWithUser) { - const keycloakUserId = request.user.sub; - - if (!keycloakUserId) { - throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); - } - - const keycloakAttributes = - await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); - const dbAttributes = - await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); - - return new HttpSuccess({ - keycloakAttributes, - databaseAttributes: dbAttributes, - }); - } - - /** - * Sync attributes for a specific profile (Admin only) - * - * @summary Sync profileId and rootDnaId to Keycloak by profile ID (ADMIN) - * - * @param {string} profileId Profile ID - * @param {string} profileType Profile type (PROFILE or PROFILE_EMPLOYEE) - */ - @Post("sync-profile/:profileId") - async syncByProfileId( - @Path() profileId: string, - @Query() profileType: "PROFILE" | "PROFILE_EMPLOYEE" = "PROFILE", - ) { - if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { - throw new HttpError( - HttpStatus.BAD_REQUEST, - "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", - ); - } - - const success = await this.keycloakAttributeService.syncOnOrganizationChange( - profileId, - profileType, - ); - - if (!success) { - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ หรือไม่พบข้อมูล profile", - ); - } - - return new HttpSuccess({ message: "Sync ข้อมูลสำเร็จ" }); - } - - /** - * Batch sync attributes for multiple profiles (Admin only) - * - * @summary Batch sync profileId and rootDnaId to Keycloak for multiple profiles (ADMIN) - * - * @param {request} request Request body containing profileIds array and profileType - */ - // @Post("sync-profiles-batch") - async syncByProfileIds( - @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" }, - ) { - const { profileIds, profileType } = request; - - // Validate profileIds - if (!profileIds || profileIds.length === 0) { - throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า"); - } - - // Validate profileType - if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { - throw new HttpError( - HttpStatus.BAD_REQUEST, - "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", - ); - } - - const result = { - total: profileIds.length, - success: 0, - failed: 0, - details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>, - }; - - // Process each profileId - for (const profileId of profileIds) { - try { - const success = await this.keycloakAttributeService.syncOnOrganizationChange( - profileId, - profileType, - ); - - if (success) { - result.success++; - result.details.push({ profileId, status: "success" }); - } else { - result.failed++; - result.details.push({ - profileId, - status: "failed", - error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID", - }); - } - } catch (error: any) { - result.failed++; - result.details.push({ profileId, status: "failed", error: error.message }); - } - } - - return new HttpSuccess({ - message: "Batch sync เสร็จสิ้น", - ...result, - }); - } - - /** - * Batch sync all users (Admin only) - * - * @summary Batch sync all users to Keycloak without limit (ADMIN) - * - * @description Syncs profileId and orgRootDnaId to Keycloak for all users - * that have a keycloak ID. Uses parallel processing for better performance. - */ - // @Post("sync-all") - async syncAll() { - const result = await this.keycloakAttributeService.batchSyncUsers(); - - return new HttpSuccess({ - message: "Batch sync เสร็จสิ้น", - total: result.total, - success: result.success, - failed: result.failed, - details: result.details, - }); - } - - /** - * Ensure Keycloak users exist for all profiles (Admin only) - * - * @summary Create or verify Keycloak users for all profiles in Profile and ProfileEmployee tables (ADMIN) - * - * @description - * This endpoint will: - * - Create new Keycloak users for profiles without a keycloak ID - * - Create new Keycloak users for profiles where the stored keycloak ID doesn't exist in Keycloak - * - Verify existing Keycloak users - * - Skip profiles without a citizenId - */ - // @Post("ensure-users") - async ensureAllUsers() { - const result = await this.keycloakAttributeService.batchEnsureKeycloakUsers(); - return new HttpSuccess({ - message: "Batch ensure Keycloak users เสร็จสิ้น", - ...result, - }); - } - - /** - * Clear orphaned Keycloak users (Admin only) - * - * @summary Delete Keycloak users that are not in the database (ADMIN) - * - * @description - * This endpoint will: - * - Find users in Keycloak that are not referenced in Profile or ProfileEmployee tables - * - Delete those orphaned users from Keycloak - * - Skip protected users (super_admin, admin_issue) - * - * @param {request} request Request body containing skipUsernames array - */ - // @Post("clear-orphaned-users") - async clearOrphanedUsers(@Body() request?: { skipUsernames?: string[] }) { - const skipUsernames = request?.skipUsernames || ["super_admin", "admin_issue"]; - const result = await this.keycloakAttributeService.clearOrphanedKeycloakUsers(skipUsernames); - return new HttpSuccess({ - message: "Clear orphaned Keycloak users เสร็จสิ้น", - ...result, - }); - } -} diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts deleted file mode 100644 index c8975e43..00000000 --- a/src/controllers/ScriptProfileOrgController.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { Controller, Post, Route, Security, Tags, Request } from "tsoa"; -import { AppDataSource } from "../database/data-source"; -import HttpSuccess from "../interfaces/http-success"; -import HttpStatus from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { RequestWithUser } from "../middlewares/user"; -import { MoreThanOrEqual } from "typeorm"; -import { PosMaster } from "./../entities/PosMaster"; -import axios from "axios"; -import { KeycloakSyncController } from "./KeycloakSyncController"; -import { EmployeePosMaster } from "./../entities/EmployeePosMaster"; - -interface OrgUpdatePayload { - profileId: string; - rootDnaId: string | null; - child1DnaId: string | null; - child2DnaId: string | null; - child3DnaId: string | null; - child4DnaId: string | null; - profileType: "PROFILE" | "PROFILE_EMPLOYEE"; -} - -@Route("api/v1/org/script-profile-org") -@Tags("Keycloak Sync") -@Security("bearerAuth") -export class ScriptProfileOrgController extends Controller { - private posMasterRepo = AppDataSource.getRepository(PosMaster); - private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); - - // Idempotency flag to prevent concurrent runs - private isRunning = false; - - // Configurable values - private readonly BATCH_SIZE = parseInt(process.env.CRONJOB_BATCH_SIZE || "100", 10); - private readonly UPDATE_WINDOW_HOURS = parseInt( - process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24", - 10, - ); - - @Post("update-org") - public async cronjobUpdateOrg(@Request() request: RequestWithUser) { - // Idempotency check - prevent concurrent runs - if (this.isRunning) { - console.log("cronjobUpdateOrg: Job already running, skipping this execution"); - return new HttpSuccess({ - message: "Job already running", - skipped: true, - }); - } - - this.isRunning = true; - const startTime = Date.now(); - - try { - const windowStart = new Date(Date.now() - this.UPDATE_WINDOW_HOURS * 60 * 60 * 1000); - - console.log("cronjobUpdateOrg: Starting job", { - windowHours: this.UPDATE_WINDOW_HOURS, - windowStart: windowStart.toISOString(), - batchSize: this.BATCH_SIZE, - }); - - // Query with optimized select - only fetch required fields - const [posMasters, posMasterEmployee] = await Promise.all([ - this.posMasterRepo.find({ - where: { - lastUpdatedAt: MoreThanOrEqual(windowStart), - orgRevision: { - orgRevisionIsCurrent: true, - }, - }, - relations: [ - "orgRevision", - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - ], - select: { - id: true, - current_holderId: true, - lastUpdatedAt: true, - orgRevision: { id: true }, - orgRoot: { ancestorDNA: true }, - orgChild1: { ancestorDNA: true }, - orgChild2: { ancestorDNA: true }, - orgChild3: { ancestorDNA: true }, - orgChild4: { ancestorDNA: true }, - current_holder: { id: true }, - }, - }), - this.employeePosMasterRepo.find({ - where: { - lastUpdatedAt: MoreThanOrEqual(windowStart), - orgRevision: { - orgRevisionIsCurrent: true, - }, - }, - relations: [ - "orgRevision", - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - ], - select: { - id: true, - current_holderId: true, - lastUpdatedAt: true, - orgRevision: { id: true }, - orgRoot: { ancestorDNA: true }, - orgChild1: { ancestorDNA: true }, - orgChild2: { ancestorDNA: true }, - orgChild3: { ancestorDNA: true }, - orgChild4: { ancestorDNA: true }, - current_holder: { id: true }, - }, - }), - ]); - - console.log("cronjobUpdateOrg: Database query completed", { - posMastersCount: posMasters.length, - employeePosCount: posMasterEmployee.length, - totalRecords: posMasters.length + posMasterEmployee.length, - }); - - // Build payloads with proper profile type tracking - const payloads = this.buildPayloads(posMasters, posMasterEmployee); - - if (payloads.length === 0) { - console.log("cronjobUpdateOrg: No records to process"); - return new HttpSuccess({ - message: "No records to process", - processed: 0, - }); - } - - // Update profile's org structure in leave service by calling API - console.log("cronjobUpdateOrg: Calling leave service API", { - payloadCount: payloads.length, - }); - - await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, { - headers: { - "Content-Type": "application/json", - api_key: process.env.API_KEY, - }, - timeout: 30000, // 30 second timeout - }); - - console.log("cronjobUpdateOrg: Leave service API call successful"); - - // Group profile IDs by type for proper syncing - const profileIdsByType = this.groupProfileIdsByType(payloads); - - // Sync to Keycloak with batching - const keycloakSyncController = new KeycloakSyncController(); - const syncResults = { - total: 0, - success: 0, - failed: 0, - byType: {} as Record, - }; - - // Process each profile type separately - for (const [profileType, profileIds] of Object.entries(profileIdsByType)) { - console.log(`cronjobUpdateOrg: Syncing ${profileType} profiles`, { - count: profileIds.length, - }); - - const batches = this.chunkArray(profileIds, this.BATCH_SIZE); - const typeResult = { total: profileIds.length, success: 0, failed: 0 }; - - for (let i = 0; i < batches.length; i++) { - const batch = batches[i]; - console.log( - `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`, - { - batchSize: batch.length, - batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min( - (i + 1) * this.BATCH_SIZE, - profileIds.length, - )}`, - }, - ); - - try { - const batchResult: any = await keycloakSyncController.syncByProfileIds({ - profileIds: batch, - profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", - }); - - // Extract result data if available - const resultData = (batchResult as any)?.data || batchResult; - typeResult.success += resultData.success || 0; - typeResult.failed += resultData.failed || 0; - - console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { - success: resultData.success || 0, - failed: resultData.failed || 0, - }); - } catch (error: any) { - console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { - error: error.message, - batchSize: batch.length, - }); - // Count all profiles in failed batch as failed - typeResult.failed += batch.length; - } - } - - syncResults.byType[profileType] = typeResult; - syncResults.total += typeResult.total; - syncResults.success += typeResult.success; - syncResults.failed += typeResult.failed; - } - - const duration = Date.now() - startTime; - console.log("cronjobUpdateOrg: Job completed", { - duration: `${duration}ms`, - processed: payloads.length, - syncResults, - }); - - return new HttpSuccess({ - message: "Update org completed", - processed: payloads.length, - syncResults, - duration: `${duration}ms`, - }); - } catch (error: any) { - const duration = Date.now() - startTime; - console.error("cronjobUpdateOrg: Job failed", { - duration: `${duration}ms`, - error: error.message, - stack: error.stack, - }); - throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"); - } finally { - this.isRunning = false; - } - } - - /** - * Build payloads from PosMaster and EmployeePosMaster records - * Includes proper profile type tracking for accurate Keycloak sync - */ - private buildPayloads( - posMasters: PosMaster[], - posMasterEmployee: EmployeePosMaster[], - ): OrgUpdatePayload[] { - const payloads: OrgUpdatePayload[] = []; - - // Process PosMaster records (PROFILE type) - for (const posMaster of posMasters) { - if (posMaster.current_holder && posMaster.current_holderId) { - payloads.push({ - profileId: posMaster.current_holderId, - rootDnaId: posMaster.orgRoot?.ancestorDNA || null, - child1DnaId: posMaster.orgChild1?.ancestorDNA || null, - child2DnaId: posMaster.orgChild2?.ancestorDNA || null, - child3DnaId: posMaster.orgChild3?.ancestorDNA || null, - child4DnaId: posMaster.orgChild4?.ancestorDNA || null, - profileType: "PROFILE", - }); - } - } - - // Process EmployeePosMaster records (PROFILE_EMPLOYEE type) - for (const employeePos of posMasterEmployee) { - if (employeePos.current_holder && employeePos.current_holderId) { - payloads.push({ - profileId: employeePos.current_holderId, - rootDnaId: employeePos.orgRoot?.ancestorDNA || null, - child1DnaId: employeePos.orgChild1?.ancestorDNA || null, - child2DnaId: employeePos.orgChild2?.ancestorDNA || null, - child3DnaId: employeePos.orgChild3?.ancestorDNA || null, - child4DnaId: employeePos.orgChild4?.ancestorDNA || null, - profileType: "PROFILE_EMPLOYEE", - }); - } - } - - return payloads; - } - - /** - * Group profile IDs by their type for separate Keycloak sync calls - */ - private groupProfileIdsByType(payloads: OrgUpdatePayload[]): Record { - const grouped: Record = { - PROFILE: [], - PROFILE_EMPLOYEE: [], - }; - - for (const payload of payloads) { - grouped[payload.profileType].push(payload.profileId); - } - - // Remove empty groups and deduplicate IDs within each group - const result: Record = {}; - for (const [type, ids] of Object.entries(grouped)) { - if (ids.length > 0) { - // Deduplicate while preserving order - result[type] = Array.from(new Set(ids)); - } - } - - return result; - } - - /** - * Split array into chunks of specified size - */ - private chunkArray(array: T[], chunkSize: number): T[][] { - const chunks: T[][] = []; - for (let i = 0; i < array.length; i += chunkSize) { - chunks.push(array.slice(i, i + chunkSize)); - } - return chunks; - } -} diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index 835e31f2..a81e2af9 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -4,8 +4,8 @@ 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; const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET; -// const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; -// const API_KEY = process.env.API_KEY; +const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; +const API_KEY = process.env.API_KEY; let token: string | null = null; let decoded: DecodedJwt | null = null; @@ -165,119 +165,16 @@ export async function getUserList(first = "", max = "", search = "") { if (!res) return false; if (!res.ok) { - const errorText = await res.text(); - return Boolean(console.error("Keycloak Error Response: ", errorText)); + return Boolean(console.error("Keycloak Error Response: ", await res.json())); } - // Get raw text first to handle potential JSON parsing errors - const rawText = await res.text(); - - // Log response size for debugging - console.log(`[getUserList] Response size: ${rawText.length} bytes`); - - try { - const data = JSON.parse(rawText) as any[]; - return data.map((v: Record) => ({ - id: v.id, - username: v.username, - firstName: v.firstName, - lastName: v.lastName, - email: v.email, - enabled: v.enabled, - })); - } catch (error) { - console.error(`[getUserList] Failed to parse JSON response:`); - console.error(`[getUserList] Response preview (first 500 chars):`, rawText.substring(0, 500)); - console.error(`[getUserList] Response preview (last 200 chars):`, rawText.slice(-200)); - throw new Error( - `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, - ); - } -} - -/** - * Get all keycloak users with pagination to avoid response size limits - * - * Client must have permission to manage realm's user - * - * @returns user list if success, false otherwise. - */ -export async function getAllUsersPaginated( - search: string = "", - batchSize: number = 100, -): Promise< - | Array<{ - id: string; - username: string; - firstName?: string; - lastName?: string; - email?: string; - enabled: boolean; - }> - | false -> { - const allUsers: any[] = []; - let first = 0; - let hasMore = true; - - while (hasMore) { - const res = await fetch( - `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first}&max=${batchSize}${search ? `&search=${search}` : ""}`, - { - headers: { - authorization: `Bearer ${await getToken()}`, - "content-type": `application/json`, - }, - }, - ).catch((e) => console.log("Keycloak Error: ", e)); - - if (!res) return false; - if (!res.ok) { - const errorText = await res.text(); - console.error("Keycloak Error Response: ", errorText); - return false; - } - - const rawText = await res.text(); - - try { - const batch = JSON.parse(rawText) as any[]; - - if (batch.length === 0) { - hasMore = false; - } else { - allUsers.push(...batch); - first += batch.length; - hasMore = batch.length === batchSize; - - // Log progress for large datasets - if (allUsers.length % 500 === 0) { - console.log(`[getAllUsersPaginated] Fetched ${allUsers.length} users so far...`); - } - } - } catch (error) { - console.error(`[getAllUsersPaginated] Failed to parse JSON response at offset ${first}:`); - console.error( - `[getAllUsersPaginated] Response preview (first 500 chars):`, - rawText.substring(0, 500), - ); - console.error( - `[getAllUsersPaginated] Response preview (last 200 chars):`, - rawText.slice(-200), - ); - throw new Error(`Failed to parse Keycloak response as JSON at batch starting at ${first}.`); - } - } - - console.log(`[getAllUsersPaginated] Total users fetched: ${allUsers.length}`); - - return allUsers.map((v: any) => ({ + return ((await res.json()) as any[]).map((v: Record) => ({ id: v.id, username: v.username, firstName: v.firstName, lastName: v.lastName, email: v.email, - enabled: v.enabled === true || v.enabled === "true", + enabled: v.enabled, })); } @@ -323,34 +220,17 @@ export async function getUserListOrg(first = "", max = "", search = "", userIds: if (!res) return false; if (!res.ok) { - const errorText = await res.text(); - return Boolean(console.error("Keycloak Error Response: ", errorText)); + return Boolean(console.error("Keycloak Error Response: ", await res.json())); } - // Get raw text first to handle potential JSON parsing errors - const rawText = await res.text(); - - try { - const data = JSON.parse(rawText) as any[]; - return data.map((v: Record) => ({ - id: v.id, - username: v.username, - firstName: v.firstName, - lastName: v.lastName, - email: v.email, - enabled: v.enabled, - })); - } catch (error) { - console.error(`[getUserListOrg] Failed to parse JSON response:`); - console.error( - `[getUserListOrg] Response preview (first 500 chars):`, - rawText.substring(0, 500), - ); - console.error(`[getUserListOrg] Response preview (last 200 chars):`, rawText.slice(-200)); - throw new Error( - `Failed to parse Keycloak response as JSON. Response may be truncated or malformed.`, - ); - } + return ((await res.json()) as any[]).map((v: Record) => ({ + id: v.id, + username: v.username, + firstName: v.firstName, + lastName: v.lastName, + email: v.email, + enabled: v.enabled, + })); } export async function getUserCountOrg(first = "", max = "", search = "", userIds: string[] = []) { @@ -564,12 +444,10 @@ export async function getRoles(name?: string, token?: string) { })); } - // Return single role object - return { - id: data.id, - name: data.name, - description: data.description, - }; + // return { + // id: data.id, + // name: data.name, + // }; } /** @@ -894,73 +772,6 @@ export async function changeUserPassword(userId: string, newPassword: string) { } } -/** - * Update user attributes in Keycloak - * - * @param userId - Keycloak user ID - * @param attributes - Object containing attribute names and their values (as arrays) - * @returns true if success, false otherwise - */ -export async function updateUserAttributes( - userId: string, - attributes: Record, -): Promise { - try { - // Get existing user data to preserve other attributes - const existingUser = await getUser(userId); - - if (!existingUser) { - console.error(`User ${userId} not found in Keycloak`); - return false; - } - - // Merge existing attributes with new attributes - // IMPORTANT: Spread all existing user fields to preserve firstName, lastName, email, etc. - // The Keycloak PUT endpoint performs a full update, so we must include all fields - const updatedAttributes = { - ...existingUser, - attributes: { - ...(existingUser.attributes || {}), - ...attributes, - }, - }; - - console.log( - `[updateUserAttributes] Sending to Keycloak:`, - JSON.stringify(updatedAttributes, null, 2), - ); - - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { - headers: { - authorization: `Bearer ${await getToken()}`, - "content-type": "application/json", - }, - method: "PUT", - body: JSON.stringify(updatedAttributes), - }).catch((e) => { - console.error(`[updateUserAttributes] Network error:`, e); - return null; - }); - - if (!res) { - console.error(`[updateUserAttributes] No response from Keycloak`); - return false; - } - - if (!res.ok) { - const errorText = await res.text(); - console.error(`[updateUserAttributes] Keycloak Error (${res.status}):`, errorText); - return false; - } - - console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`); - return true; - } catch (error) { - console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error); - return false; - } -} - // Function to reset password export async function resetPassword(username: string) { try { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index 9a571572..c27e6188 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -75,16 +75,6 @@ export async function expressAuthentication( request.app.locals.logData.userName = payload.name; request.app.locals.logData.user = payload.preferred_username; - // เก็บค่า profileId และ orgRootDnaId จาก token (ใช้ค่าว่างถ้าไม่มี) - request.app.locals.logData.profileId = payload.profileId ?? ""; - request.app.locals.logData.orgRootDnaId = payload.orgRootDnaId ?? ""; - request.app.locals.logData.orgChild1DnaId = payload.orgChild1DnaId ?? ""; - request.app.locals.logData.orgChild2DnaId = payload.orgChild2DnaId ?? ""; - request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? ""; - request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? ""; - request.app.locals.logData.empType = payload.empType ?? ""; - request.app.locals.logData.prefix = payload.prefix ?? ""; - return payload; } diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index 75c84d01..e5c48d9a 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -9,14 +9,6 @@ export type RequestWithUser = Request & { preferred_username: string; email: string; role: string[]; - profileId?: string; - prefix?: string; - orgRootDnaId?: string; - orgChild1DnaId?: string; - orgChild2DnaId?: string; - orgChild3DnaId?: string; - orgChild4DnaId?: string; - empType?: string; }; }; diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts deleted file mode 100644 index 5b61e286..00000000 --- a/src/services/KeycloakAttributeService.ts +++ /dev/null @@ -1,928 +0,0 @@ -import { AppDataSource } from "../database/data-source"; -import { Profile } from "../entities/Profile"; -import { ProfileEmployee } from "../entities/ProfileEmployee"; -// import { PosMaster } from "../entities/PosMaster"; -// import { EmployeePosMaster } from "../entities/EmployeePosMaster"; -// import { OrgRoot } from "../entities/OrgRoot"; -import { - createUser, - getUser, - getUserByUsername, - updateUserAttributes, - deleteUser, - getRoles, - addUserRoles, - getAllUsersPaginated, -} from "../keycloak"; -import { OrgRevision } from "../entities/OrgRevision"; - -export interface UserProfileAttributes { - profileId: string | null; - orgRootDnaId: string | null; - orgChild1DnaId: string | null; - orgChild2DnaId: string | null; - orgChild3DnaId: string | null; - orgChild4DnaId: string | null; - empType: string | null; - prefix?: string | null; -} - -/** - * Keycloak Attribute Service - * Service for syncing profileId and orgRootDnaId to Keycloak user attributes - */ -export class KeycloakAttributeService { - private profileRepo = AppDataSource.getRepository(Profile); - private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); - // private posMasterRepo = AppDataSource.getRepository(PosMaster); - // private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); - // private orgRootRepo = AppDataSource.getRepository(OrgRoot); - private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); - - /** - * Get profile attributes (profileId and orgRootDnaId) from database - * Searches in Profile table first (ข้าราชการ), then ProfileEmployee (ลูกจ้าง) - * - * @param keycloakUserId - Keycloak user ID - * @returns UserProfileAttributes with profileId and orgRootDnaId - */ - async getUserProfileAttributes(keycloakUserId: string): Promise { - // First, try to find in Profile (ข้าราชการ) - const revisionCurrent = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true }, - }); - const revisionId = revisionCurrent ? revisionCurrent.id : null; - const profileResult = await this.profileRepo - .createQueryBuilder("p") - .leftJoinAndSelect("p.current_holders", "pm") - .leftJoinAndSelect("pm.orgRoot", "orgRoot") - .leftJoinAndSelect("pm.orgChild1", "orgChild1") - .leftJoinAndSelect("pm.orgChild2", "orgChild2") - .leftJoinAndSelect("pm.orgChild3", "orgChild3") - .leftJoinAndSelect("pm.orgChild4", "orgChild4") - .where("p.keycloak = :keycloakUserId", { keycloakUserId }) - .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) - .getOne(); - - if ( - profileResult && - profileResult.current_holders && - profileResult.current_holders.length > 0 - ) { - const currentPos = profileResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - return { - profileId: profileResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: "OFFICER", - prefix: profileResult.prefix, - }; - } - - // If not found in Profile, try ProfileEmployee (ลูกจ้าง) - const profileEmployeeResult = await this.profileEmployeeRepo - .createQueryBuilder("pe") - .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") - .leftJoinAndSelect("epm.orgChild1", "orgChild1") - .leftJoinAndSelect("epm.orgChild2", "orgChild2") - .leftJoinAndSelect("epm.orgChild3", "orgChild3") - .leftJoinAndSelect("epm.orgChild4", "orgChild4") - .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) - .getOne(); - - if ( - profileEmployeeResult && - profileEmployeeResult.current_holders && - profileEmployeeResult.current_holders.length > 0 - ) { - const currentPos = profileEmployeeResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - return { - profileId: profileEmployeeResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: profileEmployeeResult.employeeClass, - prefix: profileEmployeeResult.prefix, - }; - } - - // Return null values if no profile found - return { - profileId: null, - orgRootDnaId: null, - orgChild1DnaId: null, - orgChild2DnaId: null, - orgChild3DnaId: null, - orgChild4DnaId: null, - empType: null, - prefix: null, - }; - } - - /** - * Get profile attributes by profile ID directly - * Used for syncing specific profiles - * - * @param profileId - Profile ID - * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง - * @returns UserProfileAttributes with profileId and orgRootDnaId - */ - async getAttributesByProfileId( - profileId: string, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise { - const revisionCurrent = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true }, - }); - const revisionId = revisionCurrent ? revisionCurrent.id : null; - if (profileType === "PROFILE") { - const profileResult = await this.profileRepo - .createQueryBuilder("p") - .leftJoinAndSelect("p.current_holders", "pm") - .leftJoinAndSelect("pm.orgRoot", "orgRoot") - .leftJoinAndSelect("pm.orgChild1", "orgChild1") - .leftJoinAndSelect("pm.orgChild2", "orgChild2") - .leftJoinAndSelect("pm.orgChild3", "orgChild3") - .leftJoinAndSelect("pm.orgChild4", "orgChild4") - .where("p.id = :profileId", { profileId }) - .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) - .getOne(); - - if ( - profileResult && - profileResult.current_holders && - profileResult.current_holders.length > 0 - ) { - const currentPos = profileResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - return { - profileId: profileResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: "OFFICER", - prefix: profileResult.prefix, - }; - } - } else { - const profileEmployeeResult = await this.profileEmployeeRepo - .createQueryBuilder("pe") - .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") - .leftJoinAndSelect("pm.orgChild1", "orgChild1") - .leftJoinAndSelect("pm.orgChild2", "orgChild2") - .leftJoinAndSelect("pm.orgChild3", "orgChild3") - .leftJoinAndSelect("pm.orgChild4", "orgChild4") - .where("pe.id = :profileId", { profileId }) - .getOne(); - - if ( - profileEmployeeResult && - profileEmployeeResult.current_holders && - profileEmployeeResult.current_holders.length > 0 - ) { - const currentPos = profileEmployeeResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - - return { - profileId: profileEmployeeResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: profileEmployeeResult.employeeClass, - prefix: profileEmployeeResult.prefix, - }; - } - } - - return { - profileId: null, - orgRootDnaId: null, - orgChild1DnaId: null, - orgChild2DnaId: null, - orgChild3DnaId: null, - orgChild4DnaId: null, - empType: null, - prefix: null, - }; - } - - /** - * Sync user attributes to Keycloak - * - * @param keycloakUserId - Keycloak user ID - * @returns true if sync successful, false otherwise - */ - async syncUserAttributes(keycloakUserId: string): Promise { - try { - const attributes = await this.getUserProfileAttributes(keycloakUserId); - - if (!attributes.profileId) { - console.log(`No profile found for Keycloak user ${keycloakUserId}`); - return false; - } - - // Prepare attributes for Keycloak (must be arrays) - const keycloakAttributes: Record = { - profileId: [attributes.profileId], - orgRootDnaId: [attributes.orgRootDnaId || ""], - orgChild1DnaId: [attributes.orgChild1DnaId || ""], - orgChild2DnaId: [attributes.orgChild2DnaId || ""], - orgChild3DnaId: [attributes.orgChild3DnaId || ""], - orgChild4DnaId: [attributes.orgChild4DnaId || ""], - empType: [attributes.empType || ""], - prefix: [attributes.prefix || ""], - }; - - const success = await updateUserAttributes(keycloakUserId, keycloakAttributes); - - if (success) { - console.log(`Synced attributes for Keycloak user ${keycloakUserId}:`, attributes); - } - - return success; - } catch (error) { - console.error(`Error syncing attributes for Keycloak user ${keycloakUserId}:`, error); - return false; - } - } - - /** - * Sync attributes when organization changes - * This is called when a user moves to a different organization - * - * @param profileId - Profile ID - * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง - * @returns true if sync successful, false otherwise - */ - async syncOnOrganizationChange( - profileId: string, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise { - try { - // Get the keycloak userId from the profile - let keycloakUserId: string | null = null; - - if (profileType === "PROFILE") { - const profile = await this.profileRepo.findOne({ where: { id: profileId } }); - keycloakUserId = profile?.keycloak || ""; - } else { - const profileEmployee = await this.profileEmployeeRepo.findOne({ - where: { id: profileId }, - }); - keycloakUserId = profileEmployee?.keycloak || ""; - } - - if (!keycloakUserId) { - console.log(`No Keycloak user ID found for profile ${profileId}`); - return false; - } - - return await this.syncUserAttributes(keycloakUserId); - } catch (error) { - console.error(`Error syncing organization change for profile ${profileId}:`, error); - return false; - } - } - - /** - * Batch sync multiple users with unlimited count and parallel processing - * Useful for initial sync or periodic updates - * - * @param options - Optional configuration (limit for testing, concurrency for parallel processing) - * @returns Object with success count and details - */ - async batchSyncUsers(options?: { - limit?: number; - concurrency?: number; - }): Promise<{ total: number; success: number; failed: number; details: any[] }> { - const limit = options?.limit; - const concurrency = options?.concurrency ?? 5; - - const result = { - total: 0, - success: 0, - failed: 0, - details: [] as any[], - }; - - try { - // Build query for profiles with keycloak IDs (ข้าราชการ) - const profileQuery = this.profileRepo - .createQueryBuilder("p") - .where("p.keycloak IS NOT NULL") - .andWhere("p.keycloak != :empty", { empty: "" }); - - // Build query for profileEmployees with keycloak IDs (ลูกจ้าง) - const profileEmployeeQuery = this.profileEmployeeRepo - .createQueryBuilder("pe") - .where("pe.keycloak IS NOT NULL") - .andWhere("pe.keycloak != :empty", { empty: "" }); - - // Apply limit if specified (for testing purposes) - if (limit !== undefined) { - profileQuery.take(limit); - profileEmployeeQuery.take(limit); - } - - // Get profiles from both tables - const [profiles, profileEmployees] = await Promise.all([ - profileQuery.getMany(), - profileEmployeeQuery.getMany(), - ]); - - const allProfiles = [ - ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), - ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), - ]; - - result.total = allProfiles.length; - - // Process in parallel with concurrency limit - const processedResults = await this.processInParallel( - allProfiles, - concurrency, - async ({ profile, type }, _index) => { - const keycloakUserId = profile.keycloak; - - try { - const success = await this.syncOnOrganizationChange(profile.id, type); - if (success) { - result.success++; - return { - profileId: profile.id, - keycloakUserId, - status: "success", - }; - } else { - result.failed++; - return { - profileId: profile.id, - keycloakUserId, - status: "failed", - error: "Sync returned false", - }; - } - } catch (error: any) { - result.failed++; - return { - profileId: profile.id, - keycloakUserId, - status: "error", - error: error.message, - }; - } - }, - ); - - // Separate results from errors - for (const resultItem of processedResults) { - if ("error" in resultItem) { - result.failed++; - result.details.push({ - profileId: "unknown", - keycloakUserId: "unknown", - status: "error", - error: JSON.stringify(resultItem.error), - }); - } else { - result.details.push(resultItem); - } - } - - console.log( - `Batch sync completed: total=${result.total}, success=${result.success}, failed=${result.failed}`, - ); - } catch (error) { - console.error("Error in batch sync:", error); - } - - return result; - } - - /** - * Get current Keycloak attributes for a user - * - * @param keycloakUserId - Keycloak user ID - * @returns Current attributes from Keycloak - */ - async getCurrentKeycloakAttributes( - keycloakUserId: string, - ): Promise { - try { - const user = await getUser(keycloakUserId); - - if (!user || !user.attributes) { - return null; - } - - return { - profileId: user.attributes.profileId?.[0] || "", - orgRootDnaId: user.attributes.orgRootDnaId?.[0] || "", - orgChild1DnaId: user.attributes.orgChild1DnaId?.[0] || "", - orgChild2DnaId: user.attributes.orgChild2DnaId?.[0] || "", - orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "", - orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "", - empType: user.attributes.empType?.[0] || "", - prefix: user.attributes.prefix?.[0] || "", - }; - } catch (error) { - console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error); - return null; - } - } - - /** - * Ensure Keycloak user exists for a profile - * Creates user if keycloak field is empty OR if stored keycloak ID doesn't exist in Keycloak - * - * @param profileId - Profile ID - * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' - * @returns Object with status and details - */ - async ensureKeycloakUser( - profileId: string, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise<{ - success: boolean; - action: "created" | "verified" | "skipped" | "error"; - keycloakUserId?: string; - error?: string; - }> { - try { - // Get profile from database - let profile: Profile | ProfileEmployee | null = null; - - if (profileType === "PROFILE") { - profile = await this.profileRepo.findOne({ where: { id: profileId } }); - } else { - profile = await this.profileEmployeeRepo.findOne({ where: { id: profileId } }); - } - - if (!profile) { - return { - success: false, - action: "error", - error: `Profile ${profileId} not found in database`, - }; - } - - // Check if citizenId exists - if (!profile.citizenId) { - return { - success: false, - action: "skipped", - error: "No citizenId found", - }; - } - - // Case 1: keycloak field is empty -> create new user - if (!profile.keycloak || profile.keycloak.trim() === "") { - const result = await this.createKeycloakUserFromProfile(profile, profileType); - return result; - } - - // Case 2: keycloak field is not empty -> verify user exists in Keycloak - const existingUser = await getUser(profile.keycloak); - - if (!existingUser) { - // User doesn't exist in Keycloak, create new one - console.log( - `Keycloak user ${profile.keycloak} not found in Keycloak, creating new user for profile ${profileId}`, - ); - const result = await this.createKeycloakUserFromProfile(profile, profileType); - return result; - } - - // User exists in Keycloak, verified - return { - success: true, - action: "verified", - keycloakUserId: profile.keycloak, - }; - } catch (error: any) { - console.error(`Error ensuring Keycloak user for profile ${profileId}:`, error); - return { - success: false, - action: "error", - error: error.message || "Unknown error", - }; - } - } - - /** - * Create Keycloak user from profile data - * - * @param profile - Profile or ProfileEmployee entity - * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' - * @returns Object with status and details - */ - private async createKeycloakUserFromProfile( - profile: Profile | ProfileEmployee, - profileType: "PROFILE" | "PROFILE_EMPLOYEE", - ): Promise<{ - success: boolean; - action: "created" | "verified" | "skipped" | "error"; - keycloakUserId?: string; - error?: string; - }> { - try { - // Check if user already exists by username (citizenId) - const existingUserByUsername = await getUserByUsername(profile.citizenId); - if (Array.isArray(existingUserByUsername) && existingUserByUsername.length > 0) { - // User already exists with this username, update the keycloak field - const existingUserId = existingUserByUsername[0].id; - console.log( - `User with citizenId ${profile.citizenId} already exists in Keycloak with ID ${existingUserId}`, - ); - - // Update the keycloak field in database - if (profileType === "PROFILE") { - await this.profileRepo.update(profile.id, { keycloak: existingUserId }); - } else { - await this.profileEmployeeRepo.update(profile.id, { keycloak: existingUserId }); - } - - // Assign default USER role to existing user - const userRole = await getRoles("USER"); - if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { - const roleAssigned = await addUserRoles(existingUserId, [ - { id: String(userRole.id), name: String(userRole.name) }, - ]); - if (roleAssigned) { - console.log(`Assigned USER role to existing user ${existingUserId}`); - } else { - console.warn(`Failed to assign USER role to existing user ${existingUserId}`); - } - } else { - console.warn(`USER role not found in Keycloak`); - } - - return { - success: true, - action: "verified", - keycloakUserId: existingUserId, - }; - } - - // Create new user in Keycloak - const createResult = await createUser(profile.citizenId, "P@ssw0rd", { - firstName: profile.firstName || "", - lastName: profile.lastName || "", - email: profile.email || undefined, - enabled: true, - }); - - if (!createResult || typeof createResult !== "string") { - return { - success: false, - action: "error", - error: "Failed to create user in Keycloak", - }; - } - - const keycloakUserId = createResult; - - // Update the keycloak field in database - if (profileType === "PROFILE") { - await this.profileRepo.update(profile.id, { keycloak: keycloakUserId }); - } else { - await this.profileEmployeeRepo.update(profile.id, { keycloak: keycloakUserId }); - } - - // Assign default USER role - const userRole = await getRoles("USER"); - if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { - const roleAssigned = await addUserRoles(keycloakUserId, [ - { id: String(userRole.id), name: String(userRole.name) }, - ]); - if (roleAssigned) { - console.log(`Assigned USER role to user ${keycloakUserId}`); - } else { - console.warn(`Failed to assign USER role to user ${keycloakUserId}`); - } - } else { - console.warn(`USER role not found in Keycloak`); - } - - console.log( - `Created Keycloak user for profile ${profile.id} (citizenId: ${profile.citizenId}) with ID ${keycloakUserId}`, - ); - - return { - success: true, - action: "created", - keycloakUserId, - }; - } catch (error: any) { - console.error(`Error creating Keycloak user for profile ${profile.id}:`, error); - return { - success: false, - action: "error", - error: error.message || "Unknown error", - }; - } - } - - /** - * Process items in parallel with concurrency limit - */ - private async processInParallel( - items: T[], - concurrencyLimit: number, - processor: (item: T, index: number) => Promise, - ): Promise> { - const results: Array = []; - - // Process items in batches - for (let i = 0; i < items.length; i += concurrencyLimit) { - const batch = items.slice(i, i + concurrencyLimit); - - // Process batch in parallel with error handling - const batchResults = await Promise.all( - batch.map(async (item, batchIndex) => { - try { - return await processor(item, i + batchIndex); - } catch (error) { - return { error }; - } - }), - ); - - results.push(...batchResults); - - // Log progress after each batch - const completed = Math.min(i + concurrencyLimit, items.length); - console.log(`Progress: ${completed}/${items.length}`); - } - - return results; - } - - /** - * Batch ensure Keycloak users for all profiles - * Processes all rows in Profile and ProfileEmployee tables - * - * @returns Object with total, success, failed counts and details - */ - async batchEnsureKeycloakUsers(): Promise<{ - total: number; - created: number; - verified: number; - skipped: number; - failed: number; - details: Array<{ - profileId: string; - profileType: string; - action: string; - keycloakUserId?: string; - error?: string; - }>; - }> { - const result = { - total: 0, - created: 0, - verified: 0, - skipped: 0, - failed: 0, - details: [] as Array<{ - profileId: string; - profileType: string; - action: string; - keycloakUserId?: string; - error?: string; - }>, - }; - - try { - // Get all profiles from Profile table (ข้าราชการ) - const profiles = await this.profileRepo.find({ where: { isLeave: false } }); // Only active profiles - - // Get all profiles from ProfileEmployee table (ลูกจ้าง) - const profileEmployees = await this.profileEmployeeRepo.find({ where: { isLeave: false } }); // Only active profiles - - const allProfiles = [ - ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), - ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), - ]; - - result.total = allProfiles.length; - - // Process in parallel with concurrency limit - const CONCURRENCY_LIMIT = 5; // Adjust based on environment - - const processedResults = await this.processInParallel( - allProfiles, - CONCURRENCY_LIMIT, - async ({ profile, type }) => { - const ensureResult = await this.ensureKeycloakUser(profile.id, type); - - // Update counters - switch (ensureResult.action) { - case "created": - result.created++; - break; - case "verified": - result.verified++; - break; - case "skipped": - result.skipped++; - break; - case "error": - result.failed++; - break; - } - - return { - profileId: profile.id, - profileType: type, - action: ensureResult.action, - keycloakUserId: ensureResult.keycloakUserId, - error: ensureResult.error, - }; - }, - ); - - // Separate results from errors - for (const resultItem of processedResults) { - if ("error" in resultItem) { - result.failed++; - result.details.push({ - profileId: "unknown", - profileType: "unknown", - action: "error", - error: JSON.stringify(resultItem.error), - }); - } else { - result.details.push(resultItem); - } - } - - console.log( - `Batch ensure Keycloak users completed: total=${result.total}, created=${result.created}, verified=${result.verified}, skipped=${result.skipped}, failed=${result.failed}`, - ); - } catch (error) { - console.error("Error in batch ensure Keycloak users:", error); - } - - return result; - } - - /** - * Clear orphaned Keycloak users - * Deletes users in Keycloak that are not referenced in Profile or ProfileEmployee tables - * - * @param skipUsernames - Array of usernames to skip (e.g., ['super_admin']) - * @returns Object with counts and details - */ - async clearOrphanedKeycloakUsers(skipUsernames: string[] = []): Promise<{ - totalInKeycloak: number; - totalInDatabase: number; - orphanedCount: number; - deleted: number; - skipped: number; - failed: number; - details: Array<{ - keycloakUserId: string; - username: string; - action: "deleted" | "skipped" | "error"; - error?: string; - }>; - }> { - const result = { - totalInKeycloak: 0, - totalInDatabase: 0, - orphanedCount: 0, - deleted: 0, - skipped: 0, - failed: 0, - details: [] as Array<{ - keycloakUserId: string; - username: string; - action: "deleted" | "skipped" | "error"; - error?: string; - }>, - }; - - try { - // Get all keycloak IDs from database (Profile + ProfileEmployee) - const profiles = await this.profileRepo - .createQueryBuilder("p") - .where("p.keycloak IS NOT NULL") - .andWhere("p.keycloak != :empty", { empty: "" }) - .getMany(); - - const profileEmployees = await this.profileEmployeeRepo - .createQueryBuilder("pe") - .where("pe.keycloak IS NOT NULL") - .andWhere("pe.keycloak != :empty", { empty: "" }) - .getMany(); - - // Create a Set of all keycloak IDs in database for O(1) lookup - const databaseKeycloakIds = new Set(); - for (const p of profiles) { - if (p.keycloak) databaseKeycloakIds.add(p.keycloak); - } - for (const pe of profileEmployees) { - if (pe.keycloak) databaseKeycloakIds.add(pe.keycloak); - } - - result.totalInDatabase = databaseKeycloakIds.size; - - // Get all users from Keycloak with pagination to avoid response size limits - const keycloakUsers = await getAllUsersPaginated(); - if (!keycloakUsers || typeof keycloakUsers !== "object") { - throw new Error("Failed to get users from Keycloak"); - } - - result.totalInKeycloak = keycloakUsers.length; - - // Find orphaned users (in Keycloak but not in database) - const orphanedUsers = keycloakUsers.filter((user: any) => !databaseKeycloakIds.has(user.id)); - result.orphanedCount = orphanedUsers.length; - - // Delete orphaned users (skip protected ones) - for (const user of orphanedUsers) { - const username = user.username; - const userId = user.id; - - // Check if user should be skipped - if (skipUsernames.includes(username)) { - result.skipped++; - result.details.push({ - keycloakUserId: userId, - username, - action: "skipped", - }); - continue; - } - - // Delete user from Keycloak - try { - const deleteSuccess = await deleteUser(userId); - if (deleteSuccess) { - result.deleted++; - result.details.push({ - keycloakUserId: userId, - username, - action: "deleted", - }); - } else { - result.failed++; - result.details.push({ - keycloakUserId: userId, - username, - action: "error", - error: "Failed to delete user from Keycloak", - }); - } - } catch (error: any) { - result.failed++; - result.details.push({ - keycloakUserId: userId, - username, - action: "error", - error: error.message || "Unknown error", - }); - } - } - - console.log( - `Clear orphaned Keycloak users completed: totalInKeycloak=${result.totalInKeycloak}, totalInDatabase=${result.totalInDatabase}, orphaned=${result.orphanedCount}, deleted=${result.deleted}, skipped=${result.skipped}, failed=${result.failed}`, - ); - } catch (error) { - console.error("Error in clear orphaned Keycloak users:", error); - throw error; - } - - return result; - } -} From 35b5d16292e35e45e5bb1c9cb179ed955bac09a6 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Feb 2026 07:56:04 +0700 Subject: [PATCH 127/178] fix --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 51e444cf..1b119c40 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,4 +31,4 @@ USER node # Define the entrypoint and default command # If you have a custom entrypoint script -CMD [ "node", "dist/src/app.js" ] +CMD [ "node", "dist/app.js" ] From 911d9b6bc5abb72c2a807697494ffdf0a1523a2e Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Feb 2026 09:30:46 +0700 Subject: [PATCH 128/178] fix bug error --- src/__tests__/setup.ts | 17 + src/__tests__/unit/OrgMapping.spec.ts | 54 ++ .../unit/OrganizationController.spec.ts | 460 ++++++++++++++++++ src/controllers/PermissionController.ts | 105 +--- src/database/data-source.ts | 4 +- 5 files changed, 554 insertions(+), 86 deletions(-) create mode 100644 src/__tests__/setup.ts create mode 100644 src/__tests__/unit/OrgMapping.spec.ts create mode 100644 src/__tests__/unit/OrganizationController.spec.ts diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 00000000..04ef89ef --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,17 @@ +// Test setup file for Jest +// Mock environment variables +process.env.NODE_ENV = 'test'; +process.env.DB_HOST = 'localhost'; +process.env.DB_PORT = '3306'; +process.env.DB_USERNAME = 'test'; +process.env.DB_PASSWORD = 'test'; +process.env.DB_DATABASE = 'test_db'; + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + log: jest.fn(), +}; diff --git a/src/__tests__/unit/OrgMapping.spec.ts b/src/__tests__/unit/OrgMapping.spec.ts new file mode 100644 index 00000000..c59e5697 --- /dev/null +++ b/src/__tests__/unit/OrgMapping.spec.ts @@ -0,0 +1,54 @@ +/** + * Unit tests for move-draft-to-current helper functions + */ + +import { OrgIdMapping, AllOrgMappings } from '../../interfaces/OrgMapping'; + +// Mock dependencies +jest.mock('../../database/data-source', () => ({ + AppDataSource: { + createQueryRunner: jest.fn(), + }, +})); + +describe('OrgMapping Interfaces', () => { + describe('OrgIdMapping', () => { + it('should create a valid OrgIdMapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + expect(mapping.byAncestorDNA).toBeInstanceOf(Map); + expect(mapping.byDraftId).toBeInstanceOf(Map); + }); + + it('should store and retrieve values correctly', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map([['dna1', 'id1']]), + byDraftId: new Map([['draftId1', 'currentId1']]), + }; + + expect(mapping.byAncestorDNA.get('dna1')).toBe('id1'); + expect(mapping.byDraftId.get('draftId1')).toBe('currentId1'); + }); + }); + + describe('AllOrgMappings', () => { + it('should create a valid AllOrgMappings', () => { + const mappings: AllOrgMappings = { + orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() }, + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, + }; + + expect(mappings.orgRoot).toBeDefined(); + expect(mappings.orgChild1).toBeDefined(); + expect(mappings.orgChild2).toBeDefined(); + expect(mappings.orgChild3).toBeDefined(); + expect(mappings.orgChild4).toBeDefined(); + }); + }); +}); diff --git a/src/__tests__/unit/OrganizationController.spec.ts b/src/__tests__/unit/OrganizationController.spec.ts new file mode 100644 index 00000000..615298bc --- /dev/null +++ b/src/__tests__/unit/OrganizationController.spec.ts @@ -0,0 +1,460 @@ +/** + * Unit tests for OrganizationController move-draft-to-current helper functions + */ + +import { OrgIdMapping } from '../../interfaces/OrgMapping'; + +// Mock typeorm +jest.mock('typeorm', () => ({ + Entity: jest.fn(), + Column: jest.fn(), + ManyToOne: jest.fn(), + JoinColumn: jest.fn(), + OneToMany: jest.fn(), + In: jest.fn((val: any) => val), + Like: jest.fn((val: any) => val), + IsNull: jest.fn(), + Not: jest.fn(), +})); + +// Mock entities +jest.mock('../../entities/OrgRoot', () => ({ OrgRoot: {} })); +jest.mock('../../entities/OrgChild1', () => ({ OrgChild1: {} })); +jest.mock('../../entities/OrgChild2', () => ({ OrgChild2: {} })); +jest.mock('../../entities/OrgChild3', () => ({ OrgChild3: {} })); +jest.mock('../../entities/OrgChild4', () => ({ OrgChild4: {} })); +jest.mock('../../entities/PosMaster', () => ({ PosMaster: {} })); +jest.mock('../../entities/Position', () => ({ Position: {} })); + +// Import after mocking +import { In, Like } from 'typeorm'; +import { OrgRoot } from '../../entities/OrgRoot'; +import { OrgChild1 } from '../../entities/OrgChild1'; +import { OrgChild2 } from '../../entities/OrgChild2'; +import { OrgChild3 } from '../../entities/OrgChild3'; +import { OrgChild4 } from '../../entities/OrgChild4'; +import { PosMaster } from '../../entities/PosMaster'; +import { Position } from '../../entities/Position'; + +describe('OrganizationController - Helper Functions', () => { + let mockQueryRunner: any; + let mockController: any; + + beforeEach(() => { + // Mock queryRunner + mockQueryRunner = { + manager: { + find: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }; + + // Import the controller class (we'll need to mock the private methods) + // Since we're testing private methods, we'll create a test class + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('resolveOrgId()', () => { + it('should return null when draftId is null', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + // Simulate the function logic + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId(null, mapping)).toBeNull(); + }); + + it('should return null when draftId is undefined', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + const resolveOrgId = (draftId: string | null | undefined, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId(undefined, mapping)).toBeNull(); + }); + + it('should return mapped ID when draftId exists in mapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map([['draft1', 'current1']]), + }; + + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId('draft1', mapping)).toBe('current1'); + }); + + it('should return null when draftId does not exist in mapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId('nonexistent', mapping)).toBeNull(); + }); + }); + + describe('cascadeDeletePositions()', () => { + it('should delete positions with orgRootId when entityClass is OrgRoot', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgRootId: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgRootId: 'node1', + }); + }); + + it('should delete positions with orgChild1Id when entityClass is OrgChild1', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild1Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild1Id: 'node1', + }); + }); + + it('should delete positions with orgChild2Id when entityClass is OrgChild2', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild2Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild2Id: 'node1', + }); + }); + + it('should delete positions with orgChild3Id when entityClass is OrgChild3', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild3Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild3Id: 'node1', + }); + }); + + it('should delete positions with orgChild4Id when entityClass is OrgChild4', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild4Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild4Id: 'node1', + }); + }); + }); + + describe('syncOrgLevel()', () => { + beforeEach(() => { + mockQueryRunner.manager.find.mockResolvedValue([]); + mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.create.mockReturnValue({}); + mockQueryRunner.manager.save.mockResolvedValue({}); + }); + + it('should fetch draft and current nodes with Like filter', async () => { + const repository = { + find: jest.fn().mockResolvedValue([]), + }; + + await repository.find({ + where: { + orgRevisionId: 'draftRev1', + ancestorDNA: Like('root-dna%'), + }, + }); + + await repository.find({ + where: { + orgRevisionId: 'currentRev1', + ancestorDNA: Like('root-dna%'), + }, + }); + + expect(repository.find).toHaveBeenCalledTimes(2); + }); + + it('should build lookup maps from draft and current nodes', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const draftByDNA = new Map(draftNodes.map(n => [n.ancestorDNA, n])); + const currentByDNA = new Map(currentNodes.map(n => [n.ancestorDNA, n])); + + expect(draftByDNA.size).toBe(2); + expect(currentByDNA.size).toBe(1); + expect(draftByDNA.get('root-dna/child1')).toEqual(draftNodes[0]); + expect(currentByDNA.get('root-dna/child1')).toEqual(currentNodes[0]); + }); + + it('should identify nodes to delete (in current but not in draft)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + { id: 'current2', ancestorDNA: 'root-dna/child2' }, // Not in draft + ]; + + const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n])); + const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA)); + + expect(toDelete).toHaveLength(1); + expect(toDelete[0].id).toBe('current2'); + }); + + it('should identify nodes to update (in both draft and current)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); + const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA)); + + expect(toUpdate).toHaveLength(1); + expect(toUpdate[0].id).toBe('draft1'); + }); + + it('should identify nodes to insert (in draft but not in current)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); + const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA)); + + expect(toInsert).toHaveLength(1); + expect(toInsert[0].id).toBe('draft2'); + }); + + it('should return correct mapping after sync', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map([ + ['root-dna/child1', 'current1'], + ['root-dna/child2', 'current2'], + ]), + byDraftId: new Map([ + ['draft1', 'current1'], + ['draft2', 'current2'], + ]), + }; + + expect(mapping.byAncestorDNA.get('root-dna/child1')).toBe('current1'); + expect(mapping.byDraftId.get('draft1')).toBe('current1'); + expect(mapping.byDraftId.get('draft2')).toBe('current2'); + }); + }); + + describe('syncPositionsForPosMaster()', () => { + beforeEach(() => { + mockQueryRunner.manager.find.mockResolvedValue([]); + mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.create.mockReturnValue({}); + mockQueryRunner.manager.save.mockResolvedValue({}); + }); + + it('should fetch draft and current positions for a posMaster', async () => { + const draftPosMasterId = 'draft-pos-1'; + const currentPosMasterId = 'current-pos-1'; + + mockQueryRunner.manager.find + .mockResolvedValueOnce([{ id: 'pos1', posMasterId: draftPosMasterId }]) + .mockResolvedValueOnce([{ id: 'pos2', posMasterId: currentPosMasterId }]); + + await mockQueryRunner.manager.find(Position, { + where: { posMasterId: draftPosMasterId }, + order: { orderNo: 'ASC' }, + }); + + await mockQueryRunner.manager.find(Position, { + where: { posMasterId: currentPosMasterId }, + }); + + expect(mockQueryRunner.manager.find).toHaveBeenCalledTimes(2); + }); + + it('should delete all current positions when no draft positions exist', async () => { + const currentPositions = [ + { id: 'pos1', orderNo: 1 }, + { id: 'pos2', orderNo: 2 }, + ]; + + mockQueryRunner.manager.find + .mockResolvedValueOnce([]) // No draft positions + .mockResolvedValueOnce(currentPositions); + + await mockQueryRunner.manager.delete(Position, ['pos1', 'pos2']); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(Position, ['pos1', 'pos2']); + }); + + it('should delete positions not in draft (by orderNo)', async () => { + const draftPositions = [ + { id: 'dpos1', orderNo: 1 }, + { id: 'dpos2', orderNo: 2 }, + ]; + const currentPositions = [ + { id: 'cpos1', orderNo: 1 }, + { id: 'cpos2', orderNo: 2 }, + { id: 'cpos3', orderNo: 3 }, // Not in draft + ]; + + mockQueryRunner.manager.find + .mockResolvedValueOnce(draftPositions) + .mockResolvedValueOnce(currentPositions); + + const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); + const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo)); + + expect(toDelete).toHaveLength(1); + expect(toDelete[0].id).toBe('cpos3'); + }); + + it('should update existing positions (matched by orderNo)', async () => { + const draftPositions = [ + { + id: 'dpos1', + orderNo: 1, + positionName: 'Updated Name', + positionField: 'field1', + posTypeId: 'type1', + posLevelId: 'level1', + }, + ]; + const currentPositions = [ + { id: 'cpos1', orderNo: 1, positionName: 'Old Name' }, + ]; + + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftPos = draftPositions[0]; + const current = currentByOrderNo.get(draftPos.orderNo); + + expect(current).toBeDefined(); + + if (current) { + const updateData = { + positionName: draftPos.positionName, + positionField: draftPos.positionField, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + }; + + await mockQueryRunner.manager.update(Position, current.id, updateData); + + expect(mockQueryRunner.manager.update).toHaveBeenCalledWith( + Position, + 'cpos1', + expect.objectContaining({ positionName: 'Updated Name' }) + ); + } + }); + + it('should insert new positions not in current', async () => { + const draftPositions = [ + { id: 'dpos1', orderNo: 1, positionName: 'New Position' }, + ]; + const currentPositions: any[] = []; + + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftPos = draftPositions[0]; + const current = currentByOrderNo.get(draftPos.orderNo); + const currentPosMasterId = 'current-pos-1'; + + expect(current).toBeUndefined(); + + if (!current) { + const newPosition = { + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }; + + await mockQueryRunner.manager.create(Position, newPosition); + await mockQueryRunner.manager.save(newPosition); + + expect(mockQueryRunner.manager.create).toHaveBeenCalledWith(Position, { + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }); + } + }); + }); +}); diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 065bc3bf..801d4b97 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -34,18 +34,10 @@ export class PermissionController extends Controller { @Get("") public async getPermission(@Request() request: RequestWithUser) { - const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: parseInt(REDIS_PORT as string) || 6379, - }, + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, }); - - redisClient.on("error", (err: any) => { - console.error("[REDIS] Connection error:", err.message); - }); - - await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ @@ -121,7 +113,6 @@ export class PermissionController extends Controller { roles: roleAttrData, }; redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); - await redisClient.quit(); } return new HttpSuccess(reply); } @@ -135,18 +126,10 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: parseInt(REDIS_PORT as string) || 6379, - }, + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, }); - - redisClient.on("error", (err: any) => { - console.error("[REDIS] Connection error:", err.message); - }); - - await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; @@ -246,7 +229,6 @@ export class PermissionController extends Controller { }); redisClient.setex("menu_" + profile.id, 86400, JSON.stringify(reply)); - await redisClient.quit(); } return new HttpSuccess(reply); @@ -325,18 +307,10 @@ export class PermissionController extends Controller { @Path() system: string, @Path() action: string, ) { - const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: parseInt(REDIS_PORT as string) || 6379, - }, + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, }); - - redisClient.on("error", (err: any) => { - console.error("[REDIS] Connection error:", err.message); - }); - - await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; @@ -424,7 +398,6 @@ export class PermissionController extends Controller { redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply)); } } - await redisClient.quit(); return new HttpSuccess(reply); } @@ -443,18 +416,10 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: parseInt(REDIS_PORT as string) || 6379, - }, + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, }); - - redisClient.on("error", (err: any) => { - console.error("[REDIS] Connection error:", err.message); - }); - - await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let org = this.PermissionOrg(request, system, action); @@ -534,24 +499,15 @@ export class PermissionController extends Controller { redisClient.setex("user_" + profile.id, 86400, JSON.stringify(reply)); } } - await redisClient.quit(); return new HttpSuccess(reply); } public async getPermissionFunc(@Request() request: RequestWithUser) { - const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: parseInt(REDIS_PORT as string) || 6379, - }, + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, }); - - redisClient.on("error", (err: any) => { - console.error("[REDIS] Connection error:", err.message); - }); - - await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ @@ -627,7 +583,6 @@ export class PermissionController extends Controller { roles: roleAttrData, }; redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); - await redisClient.quit(); } return reply; } @@ -655,18 +610,10 @@ export class PermissionController extends Controller { } public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) { - const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: parseInt(REDIS_PORT as string) || 6379, - }, + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, }); - - redisClient.on("error", (err: any) => { - console.error("[REDIS] Connection error:", err.message); - }); - - await redisClient.connect(); const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; @@ -753,7 +700,6 @@ export class PermissionController extends Controller { redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply)); } } - await redisClient.quit(); return reply; } @@ -836,18 +782,10 @@ export class PermissionController extends Controller { @Get("checkOrg/{keycloakId}") public async checkOrg(@Path() keycloakId: string) { - const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: parseInt(REDIS_PORT as string) || 6379, - }, + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, }); - - redisClient.on("error", (err: any) => { - console.error("[REDIS] Connection error:", err.message); - }); - - await redisClient.connect(); // const getAsync = promisify(redisClient.get).bind(redisClient); // let profileType = "OFFICER"; @@ -898,7 +836,6 @@ export class PermissionController extends Controller { }; } redisClient.setex("org_" + profile.id, 86400, JSON.stringify(reply)); //Create Redis - await redisClient.quit(); // } else { // const posMaster = await this.posMasterEmpRepository.findOne({ // where: { diff --git a/src/database/data-source.ts b/src/database/data-source.ts index 92b60aa2..e44252a1 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -49,11 +49,11 @@ export const AppDataSource = new DataSource({ entities: process.env.NODE_ENV !== "production" ? ["src/entities/**/*.ts"] - : ["dist/entities/**/*.js"], + : ["dist/entities/**/*{.ts,.js}"], migrations: process.env.NODE_ENV !== "production" ? ["src/migration/**/*.ts"] - : ["dist/migration/**/*.js"], + : ["dist/migration/**/*{.ts,.js}"], subscribers: [], logger: new MyCustomLogger(), // Connection pool settings to prevent connection exhaustion From b714dfe239633dea0b8c02b023935e59ec61a257 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Feb 2026 10:05:13 +0700 Subject: [PATCH 129/178] add script update org and sync keycloak, setup time run cronjob --- src/app.ts | 44 +- src/controllers/KeycloakSyncController.ts | 254 +++++ src/controllers/ScriptProfileOrgController.ts | 326 ++++++ src/keycloak/index.ts | 139 +++ src/middlewares/auth.ts | 10 + src/middlewares/user.ts | 8 + src/services/KeycloakAttributeService.ts | 928 ++++++++++++++++++ 7 files changed, 1694 insertions(+), 15 deletions(-) create mode 100644 src/controllers/KeycloakSyncController.ts create mode 100644 src/controllers/ScriptProfileOrgController.ts create mode 100644 src/services/KeycloakAttributeService.ts diff --git a/src/app.ts b/src/app.ts index 75d0bfea..06f76548 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,6 +15,7 @@ import { logMemoryStore } from "./utils/LogMemoryStore"; import { orgStructureCache } from "./utils/OrgStructureCache"; import { CommandController } from "./controllers/CommandController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; +import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgController"; import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; @@ -52,19 +53,8 @@ async function main() { const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_PORT = +(process.env.APP_PORT || 3000); - const cronTime = "0 0 3 * * *"; // ตั้งเวลาทุกวันเวลา 03:00:00 - // const cronTime = "*/10 * * * * *"; - cron.schedule(cronTime, async () => { - try { - const orgController = new OrganizationController(); - await orgController.cronjobRevision(); - } catch (error) { - console.error("Error executing function from controller:", error); - } - }); - - const cronTime_command = "0 0 2 * * *"; - // const cronTime_command = "*/10 * * * * *"; + // Cron job for executing command - every day at 00:30:00 + const cronTime_command = "0 30 0 * * *"; cron.schedule(cronTime_command, async () => { try { const commandController = new CommandController(); @@ -74,7 +64,19 @@ async function main() { } }); - const cronTime_Oct = "0 0 1 10 *"; + // Cron job for updating org revision - every day at 01:00:00 + const cronTime = "0 0 1 * * *"; + cron.schedule(cronTime, async () => { + try { + const orgController = new OrganizationController(); + await orgController.cronjobRevision(); + } catch (error) { + console.error("Error executing function from controller:", error); + } + }); + + // Cron job for updating retirement status - every day at 02:00:00 on the 1st of October + const cronTime_Oct = "0 0 2 10 *"; cron.schedule(cronTime_Oct, async () => { try { const commandController = new CommandController(); @@ -84,7 +86,19 @@ async function main() { } }); - const cronTime_Tenure = "0 0 0 * * *"; + // Cron job for updating org DNA - every day at 03:00:00 + const cronTime_UpdateOrg = "0 0 3 * * *"; + cron.schedule(cronTime_UpdateOrg, async () => { + try { + const scriptProfileOrgController = new ScriptProfileOrgController(); + await scriptProfileOrgController.cronjobUpdateOrg({} as any); + } catch (error) { + console.error("Error executing cronjobUpdateOrg:", error); + } + }); + + // Cron job for updating tenure - every day at 04:00:00 + const cronTime_Tenure = "0 0 4 * * *"; cron.schedule(cronTime_Tenure, async () => { try { const profileSalaryController = new ProfileSalaryController(); diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts new file mode 100644 index 00000000..81dbbf66 --- /dev/null +++ b/src/controllers/KeycloakSyncController.ts @@ -0,0 +1,254 @@ +import { + Controller, + Post, + Get, + Route, + Security, + Tags, + Path, + Request, + Response, + Query, + Body, +} from "tsoa"; +import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; + +@Route("api/v1/org/keycloak-sync") +@Tags("Keycloak Sync") +@Security("bearerAuth") +@Response( + HttpStatus.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถดำเนินการได้ กรุณาลองใหม่ในภายหลัง", +) +export class KeycloakSyncController extends Controller { + private keycloakAttributeService = new KeycloakAttributeService(); + + /** + * Sync attributes for the current logged-in user + * + * @summary Sync profileId and rootDnaId to Keycloak for current user + */ + @Post("sync-me") + async syncCurrentUser(@Request() request: RequestWithUser) { + const keycloakUserId = request.user.sub; + + if (!keycloakUserId) { + throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); + } + + // Get attributes from database before sync + const dbAttrs = await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); + + const success = await this.keycloakAttributeService.syncUserAttributes(keycloakUserId); + + if (!success) { + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ กรุณาติดต่อผู้ดูแลระบบ", + ); + } + + // Verify sync by fetching attributes from Keycloak after update + const kcAttrsAfter = + await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); + + return new HttpSuccess({ + message: "Sync ข้อมูลสำเร็จ", + syncedToKeycloak: !!kcAttrsAfter?.profileId, + databaseAttributes: dbAttrs, + keycloakAttributesAfter: kcAttrsAfter, + }); + } + + /** + * Get current attributes of the logged-in user + * + * @summary Get current profileId and rootDnaId from Keycloak + */ + @Get("my-attributes") + async getMyAttributes(@Request() request: RequestWithUser) { + const keycloakUserId = request.user.sub; + + if (!keycloakUserId) { + throw new HttpError(HttpStatus.UNAUTHORIZED, "ไม่พบ Keycloak user ID"); + } + + const keycloakAttributes = + await this.keycloakAttributeService.getCurrentKeycloakAttributes(keycloakUserId); + const dbAttributes = + await this.keycloakAttributeService.getUserProfileAttributes(keycloakUserId); + + return new HttpSuccess({ + keycloakAttributes, + databaseAttributes: dbAttributes, + }); + } + + /** + * Sync attributes for a specific profile (Admin only) + * + * @summary Sync profileId and rootDnaId to Keycloak by profile ID (ADMIN) + * + * @param {string} profileId Profile ID + * @param {string} profileType Profile type (PROFILE or PROFILE_EMPLOYEE) + */ + @Post("sync-profile/:profileId") + async syncByProfileId( + @Path() profileId: string, + @Query() profileType: "PROFILE" | "PROFILE_EMPLOYEE" = "PROFILE", + ) { + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + const success = await this.keycloakAttributeService.syncOnOrganizationChange( + profileId, + profileType, + ); + + if (!success) { + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "ไม่สามารถ sync ข้อมูลไปยัง Keycloak ได้ หรือไม่พบข้อมูล profile", + ); + } + + return new HttpSuccess({ message: "Sync ข้อมูลสำเร็จ" }); + } + + /** + * Batch sync attributes for multiple profiles (Admin only) + * + * @summary Batch sync profileId and rootDnaId to Keycloak for multiple profiles (ADMIN) + * + * @param {request} request Request body containing profileIds array and profileType + */ + @Post("sync-profiles-batch") + async syncByProfileIds( + @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" }, + ) { + const { profileIds, profileType } = request; + + // Validate profileIds + if (!profileIds || profileIds.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า"); + } + + // Validate profileType + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + const result = { + total: profileIds.length, + success: 0, + failed: 0, + details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>, + }; + + // Process each profileId + for (const profileId of profileIds) { + try { + const success = await this.keycloakAttributeService.syncOnOrganizationChange( + profileId, + profileType, + ); + + if (success) { + result.success++; + result.details.push({ profileId, status: "success" }); + } else { + result.failed++; + result.details.push({ + profileId, + status: "failed", + error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ profileId, status: "failed", error: error.message }); + } + } + + return new HttpSuccess({ + message: "Batch sync เสร็จสิ้น", + ...result, + }); + } + + /** + * Batch sync all users (Admin only) + * + * @summary Batch sync all users to Keycloak without limit (ADMIN) + * + * @description Syncs profileId and orgRootDnaId to Keycloak for all users + * that have a keycloak ID. Uses parallel processing for better performance. + */ + @Post("sync-all") + async syncAll() { + const result = await this.keycloakAttributeService.batchSyncUsers(); + + return new HttpSuccess({ + message: "Batch sync เสร็จสิ้น", + total: result.total, + success: result.success, + failed: result.failed, + details: result.details, + }); + } + + /** + * Ensure Keycloak users exist for all profiles (Admin only) + * + * @summary Create or verify Keycloak users for all profiles in Profile and ProfileEmployee tables (ADMIN) + * + * @description + * This endpoint will: + * - Create new Keycloak users for profiles without a keycloak ID + * - Create new Keycloak users for profiles where the stored keycloak ID doesn't exist in Keycloak + * - Verify existing Keycloak users + * - Skip profiles without a citizenId + */ + @Post("ensure-users") + async ensureAllUsers() { + const result = await this.keycloakAttributeService.batchEnsureKeycloakUsers(); + return new HttpSuccess({ + message: "Batch ensure Keycloak users เสร็จสิ้น", + ...result, + }); + } + + /** + * Clear orphaned Keycloak users (Admin only) + * + * @summary Delete Keycloak users that are not in the database (ADMIN) + * + * @description + * This endpoint will: + * - Find users in Keycloak that are not referenced in Profile or ProfileEmployee tables + * - Delete those orphaned users from Keycloak + * - Skip protected users (super_admin, admin_issue) + * + * @param {request} request Request body containing skipUsernames array + */ + @Post("clear-orphaned-users") + async clearOrphanedUsers(@Body() request?: { skipUsernames?: string[] }) { + const skipUsernames = request?.skipUsernames || ["super_admin", "admin_issue"]; + const result = await this.keycloakAttributeService.clearOrphanedKeycloakUsers(skipUsernames); + return new HttpSuccess({ + message: "Clear orphaned Keycloak users เสร็จสิ้น", + ...result, + }); + } +} diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts new file mode 100644 index 00000000..c8975e43 --- /dev/null +++ b/src/controllers/ScriptProfileOrgController.ts @@ -0,0 +1,326 @@ +import { Controller, Post, Route, Security, Tags, Request } from "tsoa"; +import { AppDataSource } from "../database/data-source"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; +import { MoreThanOrEqual } from "typeorm"; +import { PosMaster } from "./../entities/PosMaster"; +import axios from "axios"; +import { KeycloakSyncController } from "./KeycloakSyncController"; +import { EmployeePosMaster } from "./../entities/EmployeePosMaster"; + +interface OrgUpdatePayload { + profileId: string; + rootDnaId: string | null; + child1DnaId: string | null; + child2DnaId: string | null; + child3DnaId: string | null; + child4DnaId: string | null; + profileType: "PROFILE" | "PROFILE_EMPLOYEE"; +} + +@Route("api/v1/org/script-profile-org") +@Tags("Keycloak Sync") +@Security("bearerAuth") +export class ScriptProfileOrgController extends Controller { + private posMasterRepo = AppDataSource.getRepository(PosMaster); + private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); + + // Idempotency flag to prevent concurrent runs + private isRunning = false; + + // Configurable values + private readonly BATCH_SIZE = parseInt(process.env.CRONJOB_BATCH_SIZE || "100", 10); + private readonly UPDATE_WINDOW_HOURS = parseInt( + process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24", + 10, + ); + + @Post("update-org") + public async cronjobUpdateOrg(@Request() request: RequestWithUser) { + // Idempotency check - prevent concurrent runs + if (this.isRunning) { + console.log("cronjobUpdateOrg: Job already running, skipping this execution"); + return new HttpSuccess({ + message: "Job already running", + skipped: true, + }); + } + + this.isRunning = true; + const startTime = Date.now(); + + try { + const windowStart = new Date(Date.now() - this.UPDATE_WINDOW_HOURS * 60 * 60 * 1000); + + console.log("cronjobUpdateOrg: Starting job", { + windowHours: this.UPDATE_WINDOW_HOURS, + windowStart: windowStart.toISOString(), + batchSize: this.BATCH_SIZE, + }); + + // Query with optimized select - only fetch required fields + const [posMasters, posMasterEmployee] = await Promise.all([ + this.posMasterRepo.find({ + where: { + lastUpdatedAt: MoreThanOrEqual(windowStart), + orgRevision: { + orgRevisionIsCurrent: true, + }, + }, + relations: [ + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], + select: { + id: true, + current_holderId: true, + lastUpdatedAt: true, + orgRevision: { id: true }, + orgRoot: { ancestorDNA: true }, + orgChild1: { ancestorDNA: true }, + orgChild2: { ancestorDNA: true }, + orgChild3: { ancestorDNA: true }, + orgChild4: { ancestorDNA: true }, + current_holder: { id: true }, + }, + }), + this.employeePosMasterRepo.find({ + where: { + lastUpdatedAt: MoreThanOrEqual(windowStart), + orgRevision: { + orgRevisionIsCurrent: true, + }, + }, + relations: [ + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], + select: { + id: true, + current_holderId: true, + lastUpdatedAt: true, + orgRevision: { id: true }, + orgRoot: { ancestorDNA: true }, + orgChild1: { ancestorDNA: true }, + orgChild2: { ancestorDNA: true }, + orgChild3: { ancestorDNA: true }, + orgChild4: { ancestorDNA: true }, + current_holder: { id: true }, + }, + }), + ]); + + console.log("cronjobUpdateOrg: Database query completed", { + posMastersCount: posMasters.length, + employeePosCount: posMasterEmployee.length, + totalRecords: posMasters.length + posMasterEmployee.length, + }); + + // Build payloads with proper profile type tracking + const payloads = this.buildPayloads(posMasters, posMasterEmployee); + + if (payloads.length === 0) { + console.log("cronjobUpdateOrg: No records to process"); + return new HttpSuccess({ + message: "No records to process", + processed: 0, + }); + } + + // Update profile's org structure in leave service by calling API + console.log("cronjobUpdateOrg: Calling leave service API", { + payloadCount: payloads.length, + }); + + await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, { + headers: { + "Content-Type": "application/json", + api_key: process.env.API_KEY, + }, + timeout: 30000, // 30 second timeout + }); + + console.log("cronjobUpdateOrg: Leave service API call successful"); + + // Group profile IDs by type for proper syncing + const profileIdsByType = this.groupProfileIdsByType(payloads); + + // Sync to Keycloak with batching + const keycloakSyncController = new KeycloakSyncController(); + const syncResults = { + total: 0, + success: 0, + failed: 0, + byType: {} as Record, + }; + + // Process each profile type separately + for (const [profileType, profileIds] of Object.entries(profileIdsByType)) { + console.log(`cronjobUpdateOrg: Syncing ${profileType} profiles`, { + count: profileIds.length, + }); + + const batches = this.chunkArray(profileIds, this.BATCH_SIZE); + const typeResult = { total: profileIds.length, success: 0, failed: 0 }; + + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + console.log( + `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`, + { + batchSize: batch.length, + batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min( + (i + 1) * this.BATCH_SIZE, + profileIds.length, + )}`, + }, + ); + + try { + const batchResult: any = await keycloakSyncController.syncByProfileIds({ + profileIds: batch, + profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", + }); + + // Extract result data if available + const resultData = (batchResult as any)?.data || batchResult; + typeResult.success += resultData.success || 0; + typeResult.failed += resultData.failed || 0; + + console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { + success: resultData.success || 0, + failed: resultData.failed || 0, + }); + } catch (error: any) { + console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { + error: error.message, + batchSize: batch.length, + }); + // Count all profiles in failed batch as failed + typeResult.failed += batch.length; + } + } + + syncResults.byType[profileType] = typeResult; + syncResults.total += typeResult.total; + syncResults.success += typeResult.success; + syncResults.failed += typeResult.failed; + } + + const duration = Date.now() - startTime; + console.log("cronjobUpdateOrg: Job completed", { + duration: `${duration}ms`, + processed: payloads.length, + syncResults, + }); + + return new HttpSuccess({ + message: "Update org completed", + processed: payloads.length, + syncResults, + duration: `${duration}ms`, + }); + } catch (error: any) { + const duration = Date.now() - startTime; + console.error("cronjobUpdateOrg: Job failed", { + duration: `${duration}ms`, + error: error.message, + stack: error.stack, + }); + throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"); + } finally { + this.isRunning = false; + } + } + + /** + * Build payloads from PosMaster and EmployeePosMaster records + * Includes proper profile type tracking for accurate Keycloak sync + */ + private buildPayloads( + posMasters: PosMaster[], + posMasterEmployee: EmployeePosMaster[], + ): OrgUpdatePayload[] { + const payloads: OrgUpdatePayload[] = []; + + // Process PosMaster records (PROFILE type) + for (const posMaster of posMasters) { + if (posMaster.current_holder && posMaster.current_holderId) { + payloads.push({ + profileId: posMaster.current_holderId, + rootDnaId: posMaster.orgRoot?.ancestorDNA || null, + child1DnaId: posMaster.orgChild1?.ancestorDNA || null, + child2DnaId: posMaster.orgChild2?.ancestorDNA || null, + child3DnaId: posMaster.orgChild3?.ancestorDNA || null, + child4DnaId: posMaster.orgChild4?.ancestorDNA || null, + profileType: "PROFILE", + }); + } + } + + // Process EmployeePosMaster records (PROFILE_EMPLOYEE type) + for (const employeePos of posMasterEmployee) { + if (employeePos.current_holder && employeePos.current_holderId) { + payloads.push({ + profileId: employeePos.current_holderId, + rootDnaId: employeePos.orgRoot?.ancestorDNA || null, + child1DnaId: employeePos.orgChild1?.ancestorDNA || null, + child2DnaId: employeePos.orgChild2?.ancestorDNA || null, + child3DnaId: employeePos.orgChild3?.ancestorDNA || null, + child4DnaId: employeePos.orgChild4?.ancestorDNA || null, + profileType: "PROFILE_EMPLOYEE", + }); + } + } + + return payloads; + } + + /** + * Group profile IDs by their type for separate Keycloak sync calls + */ + private groupProfileIdsByType(payloads: OrgUpdatePayload[]): Record { + const grouped: Record = { + PROFILE: [], + PROFILE_EMPLOYEE: [], + }; + + for (const payload of payloads) { + grouped[payload.profileType].push(payload.profileId); + } + + // Remove empty groups and deduplicate IDs within each group + const result: Record = {}; + for (const [type, ids] of Object.entries(grouped)) { + if (ids.length > 0) { + // Deduplicate while preserving order + result[type] = Array.from(new Set(ids)); + } + } + + return result; + } + + /** + * Split array into chunks of specified size + */ + private chunkArray(array: T[], chunkSize: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + return chunks; + } +} diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index a81e2af9..85724eec 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -832,3 +832,142 @@ export async function resetPassword(username: string) { return false; } } + +export async function updateUserAttributes( + userId: string, + attributes: Record, +): Promise { + try { + // Get existing user data to preserve other attributes + const existingUser = await getUser(userId); + + if (!existingUser) { + console.error(`User ${userId} not found in Keycloak`); + return false; + } + + // Merge existing attributes with new attributes + // IMPORTANT: Spread all existing user fields to preserve firstName, lastName, email, etc. + // The Keycloak PUT endpoint performs a full update, so we must include all fields + const updatedAttributes = { + ...existingUser, + attributes: { + ...(existingUser.attributes || {}), + ...attributes, + }, + }; + + console.log( + `[updateUserAttributes] Sending to Keycloak:`, + JSON.stringify(updatedAttributes, null, 2), + ); + + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { + headers: { + authorization: `Bearer ${await getToken()}`, + "content-type": "application/json", + }, + method: "PUT", + body: JSON.stringify(updatedAttributes), + }).catch((e) => { + console.error(`[updateUserAttributes] Network error:`, e); + return null; + }); + + if (!res) { + console.error(`[updateUserAttributes] No response from Keycloak`); + return false; + } + + if (!res.ok) { + const errorText = await res.text(); + console.error(`[updateUserAttributes] Keycloak Error (${res.status}):`, errorText); + return false; + } + + console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`); + return true; + } catch (error) { + console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error); + return false; + } +} + +export async function getAllUsersPaginated( + search: string = "", + batchSize: number = 100, +): Promise< + | Array<{ + id: string; + username: string; + firstName?: string; + lastName?: string; + email?: string; + enabled: boolean; + }> + | false +> { + const allUsers: any[] = []; + let first = 0; + let hasMore = true; + + while (hasMore) { + const res = await fetch( + `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first}&max=${batchSize}${search ? `&search=${search}` : ""}`, + { + headers: { + authorization: `Bearer ${await getToken()}`, + "content-type": `application/json`, + }, + }, + ).catch((e) => console.log("Keycloak Error: ", e)); + + if (!res) return false; + if (!res.ok) { + const errorText = await res.text(); + console.error("Keycloak Error Response: ", errorText); + return false; + } + + const rawText = await res.text(); + + try { + const batch = JSON.parse(rawText) as any[]; + + if (batch.length === 0) { + hasMore = false; + } else { + allUsers.push(...batch); + first += batch.length; + hasMore = batch.length === batchSize; + + // Log progress for large datasets + if (allUsers.length % 500 === 0) { + console.log(`[getAllUsersPaginated] Fetched ${allUsers.length} users so far...`); + } + } + } catch (error) { + console.error(`[getAllUsersPaginated] Failed to parse JSON response at offset ${first}:`); + console.error( + `[getAllUsersPaginated] Response preview (first 500 chars):`, + rawText.substring(0, 500), + ); + console.error( + `[getAllUsersPaginated] Response preview (last 200 chars):`, + rawText.slice(-200), + ); + throw new Error(`Failed to parse Keycloak response as JSON at batch starting at ${first}.`); + } + } + + console.log(`[getAllUsersPaginated] Total users fetched: ${allUsers.length}`); + + return allUsers.map((v: any) => ({ + id: v.id, + username: v.username, + firstName: v.firstName, + lastName: v.lastName, + email: v.email, + enabled: v.enabled === true || v.enabled === "true", + })); +} diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index c27e6188..9a571572 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -75,6 +75,16 @@ export async function expressAuthentication( request.app.locals.logData.userName = payload.name; request.app.locals.logData.user = payload.preferred_username; + // เก็บค่า profileId และ orgRootDnaId จาก token (ใช้ค่าว่างถ้าไม่มี) + request.app.locals.logData.profileId = payload.profileId ?? ""; + request.app.locals.logData.orgRootDnaId = payload.orgRootDnaId ?? ""; + request.app.locals.logData.orgChild1DnaId = payload.orgChild1DnaId ?? ""; + request.app.locals.logData.orgChild2DnaId = payload.orgChild2DnaId ?? ""; + request.app.locals.logData.orgChild3DnaId = payload.orgChild3DnaId ?? ""; + request.app.locals.logData.orgChild4DnaId = payload.orgChild4DnaId ?? ""; + request.app.locals.logData.empType = payload.empType ?? ""; + request.app.locals.logData.prefix = payload.prefix ?? ""; + return payload; } diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index e5c48d9a..75c84d01 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -9,6 +9,14 @@ export type RequestWithUser = Request & { preferred_username: string; email: string; role: string[]; + profileId?: string; + prefix?: string; + orgRootDnaId?: string; + orgChild1DnaId?: string; + orgChild2DnaId?: string; + orgChild3DnaId?: string; + orgChild4DnaId?: string; + empType?: string; }; }; diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts new file mode 100644 index 00000000..5b61e286 --- /dev/null +++ b/src/services/KeycloakAttributeService.ts @@ -0,0 +1,928 @@ +import { AppDataSource } from "../database/data-source"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +// import { PosMaster } from "../entities/PosMaster"; +// import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +// import { OrgRoot } from "../entities/OrgRoot"; +import { + createUser, + getUser, + getUserByUsername, + updateUserAttributes, + deleteUser, + getRoles, + addUserRoles, + getAllUsersPaginated, +} from "../keycloak"; +import { OrgRevision } from "../entities/OrgRevision"; + +export interface UserProfileAttributes { + profileId: string | null; + orgRootDnaId: string | null; + orgChild1DnaId: string | null; + orgChild2DnaId: string | null; + orgChild3DnaId: string | null; + orgChild4DnaId: string | null; + empType: string | null; + prefix?: string | null; +} + +/** + * Keycloak Attribute Service + * Service for syncing profileId and orgRootDnaId to Keycloak user attributes + */ +export class KeycloakAttributeService { + private profileRepo = AppDataSource.getRepository(Profile); + private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); + // private posMasterRepo = AppDataSource.getRepository(PosMaster); + // private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); + // private orgRootRepo = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); + + /** + * Get profile attributes (profileId and orgRootDnaId) from database + * Searches in Profile table first (ข้าราชการ), then ProfileEmployee (ลูกจ้าง) + * + * @param keycloakUserId - Keycloak user ID + * @returns UserProfileAttributes with profileId and orgRootDnaId + */ + async getUserProfileAttributes(keycloakUserId: string): Promise { + // First, try to find in Profile (ข้าราชการ) + const revisionCurrent = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true }, + }); + const revisionId = revisionCurrent ? revisionCurrent.id : null; + const profileResult = await this.profileRepo + .createQueryBuilder("p") + .leftJoinAndSelect("p.current_holders", "pm") + .leftJoinAndSelect("pm.orgRoot", "orgRoot") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("p.keycloak = :keycloakUserId", { keycloakUserId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) + .getOne(); + + if ( + profileResult && + profileResult.current_holders && + profileResult.current_holders.length > 0 + ) { + const currentPos = profileResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: "OFFICER", + prefix: profileResult.prefix, + }; + } + + // If not found in Profile, try ProfileEmployee (ลูกจ้าง) + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("epm.orgChild1", "orgChild1") + .leftJoinAndSelect("epm.orgChild2", "orgChild2") + .leftJoinAndSelect("epm.orgChild3", "orgChild3") + .leftJoinAndSelect("epm.orgChild4", "orgChild4") + .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + + // Return null values if no profile found + return { + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + prefix: null, + }; + } + + /** + * Get profile attributes by profile ID directly + * Used for syncing specific profiles + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง + * @returns UserProfileAttributes with profileId and orgRootDnaId + */ + async getAttributesByProfileId( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise { + const revisionCurrent = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true }, + }); + const revisionId = revisionCurrent ? revisionCurrent.id : null; + if (profileType === "PROFILE") { + const profileResult = await this.profileRepo + .createQueryBuilder("p") + .leftJoinAndSelect("p.current_holders", "pm") + .leftJoinAndSelect("pm.orgRoot", "orgRoot") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("p.id = :profileId", { profileId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) + .getOne(); + + if ( + profileResult && + profileResult.current_holders && + profileResult.current_holders.length > 0 + ) { + const currentPos = profileResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: "OFFICER", + prefix: profileResult.prefix, + }; + } + } else { + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("pm.orgChild1", "orgChild1") + .leftJoinAndSelect("pm.orgChild2", "orgChild2") + .leftJoinAndSelect("pm.orgChild3", "orgChild3") + .leftJoinAndSelect("pm.orgChild4", "orgChild4") + .where("pe.id = :profileId", { profileId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + } + + return { + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + prefix: null, + }; + } + + /** + * Sync user attributes to Keycloak + * + * @param keycloakUserId - Keycloak user ID + * @returns true if sync successful, false otherwise + */ + async syncUserAttributes(keycloakUserId: string): Promise { + try { + const attributes = await this.getUserProfileAttributes(keycloakUserId); + + if (!attributes.profileId) { + console.log(`No profile found for Keycloak user ${keycloakUserId}`); + return false; + } + + // Prepare attributes for Keycloak (must be arrays) + const keycloakAttributes: Record = { + profileId: [attributes.profileId], + orgRootDnaId: [attributes.orgRootDnaId || ""], + orgChild1DnaId: [attributes.orgChild1DnaId || ""], + orgChild2DnaId: [attributes.orgChild2DnaId || ""], + orgChild3DnaId: [attributes.orgChild3DnaId || ""], + orgChild4DnaId: [attributes.orgChild4DnaId || ""], + empType: [attributes.empType || ""], + prefix: [attributes.prefix || ""], + }; + + const success = await updateUserAttributes(keycloakUserId, keycloakAttributes); + + if (success) { + console.log(`Synced attributes for Keycloak user ${keycloakUserId}:`, attributes); + } + + return success; + } catch (error) { + console.error(`Error syncing attributes for Keycloak user ${keycloakUserId}:`, error); + return false; + } + } + + /** + * Sync attributes when organization changes + * This is called when a user moves to a different organization + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง + * @returns true if sync successful, false otherwise + */ + async syncOnOrganizationChange( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise { + try { + // Get the keycloak userId from the profile + let keycloakUserId: string | null = null; + + if (profileType === "PROFILE") { + const profile = await this.profileRepo.findOne({ where: { id: profileId } }); + keycloakUserId = profile?.keycloak || ""; + } else { + const profileEmployee = await this.profileEmployeeRepo.findOne({ + where: { id: profileId }, + }); + keycloakUserId = profileEmployee?.keycloak || ""; + } + + if (!keycloakUserId) { + console.log(`No Keycloak user ID found for profile ${profileId}`); + return false; + } + + return await this.syncUserAttributes(keycloakUserId); + } catch (error) { + console.error(`Error syncing organization change for profile ${profileId}:`, error); + return false; + } + } + + /** + * Batch sync multiple users with unlimited count and parallel processing + * Useful for initial sync or periodic updates + * + * @param options - Optional configuration (limit for testing, concurrency for parallel processing) + * @returns Object with success count and details + */ + async batchSyncUsers(options?: { + limit?: number; + concurrency?: number; + }): Promise<{ total: number; success: number; failed: number; details: any[] }> { + const limit = options?.limit; + const concurrency = options?.concurrency ?? 5; + + const result = { + total: 0, + success: 0, + failed: 0, + details: [] as any[], + }; + + try { + // Build query for profiles with keycloak IDs (ข้าราชการ) + const profileQuery = this.profileRepo + .createQueryBuilder("p") + .where("p.keycloak IS NOT NULL") + .andWhere("p.keycloak != :empty", { empty: "" }); + + // Build query for profileEmployees with keycloak IDs (ลูกจ้าง) + const profileEmployeeQuery = this.profileEmployeeRepo + .createQueryBuilder("pe") + .where("pe.keycloak IS NOT NULL") + .andWhere("pe.keycloak != :empty", { empty: "" }); + + // Apply limit if specified (for testing purposes) + if (limit !== undefined) { + profileQuery.take(limit); + profileEmployeeQuery.take(limit); + } + + // Get profiles from both tables + const [profiles, profileEmployees] = await Promise.all([ + profileQuery.getMany(), + profileEmployeeQuery.getMany(), + ]); + + const allProfiles = [ + ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), + ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), + ]; + + result.total = allProfiles.length; + + // Process in parallel with concurrency limit + const processedResults = await this.processInParallel( + allProfiles, + concurrency, + async ({ profile, type }, _index) => { + const keycloakUserId = profile.keycloak; + + try { + const success = await this.syncOnOrganizationChange(profile.id, type); + if (success) { + result.success++; + return { + profileId: profile.id, + keycloakUserId, + status: "success", + }; + } else { + result.failed++; + return { + profileId: profile.id, + keycloakUserId, + status: "failed", + error: "Sync returned false", + }; + } + } catch (error: any) { + result.failed++; + return { + profileId: profile.id, + keycloakUserId, + status: "error", + error: error.message, + }; + } + }, + ); + + // Separate results from errors + for (const resultItem of processedResults) { + if ("error" in resultItem) { + result.failed++; + result.details.push({ + profileId: "unknown", + keycloakUserId: "unknown", + status: "error", + error: JSON.stringify(resultItem.error), + }); + } else { + result.details.push(resultItem); + } + } + + console.log( + `Batch sync completed: total=${result.total}, success=${result.success}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in batch sync:", error); + } + + return result; + } + + /** + * Get current Keycloak attributes for a user + * + * @param keycloakUserId - Keycloak user ID + * @returns Current attributes from Keycloak + */ + async getCurrentKeycloakAttributes( + keycloakUserId: string, + ): Promise { + try { + const user = await getUser(keycloakUserId); + + if (!user || !user.attributes) { + return null; + } + + return { + profileId: user.attributes.profileId?.[0] || "", + orgRootDnaId: user.attributes.orgRootDnaId?.[0] || "", + orgChild1DnaId: user.attributes.orgChild1DnaId?.[0] || "", + orgChild2DnaId: user.attributes.orgChild2DnaId?.[0] || "", + orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "", + orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "", + empType: user.attributes.empType?.[0] || "", + prefix: user.attributes.prefix?.[0] || "", + }; + } catch (error) { + console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error); + return null; + } + } + + /** + * Ensure Keycloak user exists for a profile + * Creates user if keycloak field is empty OR if stored keycloak ID doesn't exist in Keycloak + * + * @param profileId - Profile ID + * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' + * @returns Object with status and details + */ + async ensureKeycloakUser( + profileId: string, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise<{ + success: boolean; + action: "created" | "verified" | "skipped" | "error"; + keycloakUserId?: string; + error?: string; + }> { + try { + // Get profile from database + let profile: Profile | ProfileEmployee | null = null; + + if (profileType === "PROFILE") { + profile = await this.profileRepo.findOne({ where: { id: profileId } }); + } else { + profile = await this.profileEmployeeRepo.findOne({ where: { id: profileId } }); + } + + if (!profile) { + return { + success: false, + action: "error", + error: `Profile ${profileId} not found in database`, + }; + } + + // Check if citizenId exists + if (!profile.citizenId) { + return { + success: false, + action: "skipped", + error: "No citizenId found", + }; + } + + // Case 1: keycloak field is empty -> create new user + if (!profile.keycloak || profile.keycloak.trim() === "") { + const result = await this.createKeycloakUserFromProfile(profile, profileType); + return result; + } + + // Case 2: keycloak field is not empty -> verify user exists in Keycloak + const existingUser = await getUser(profile.keycloak); + + if (!existingUser) { + // User doesn't exist in Keycloak, create new one + console.log( + `Keycloak user ${profile.keycloak} not found in Keycloak, creating new user for profile ${profileId}`, + ); + const result = await this.createKeycloakUserFromProfile(profile, profileType); + return result; + } + + // User exists in Keycloak, verified + return { + success: true, + action: "verified", + keycloakUserId: profile.keycloak, + }; + } catch (error: any) { + console.error(`Error ensuring Keycloak user for profile ${profileId}:`, error); + return { + success: false, + action: "error", + error: error.message || "Unknown error", + }; + } + } + + /** + * Create Keycloak user from profile data + * + * @param profile - Profile or ProfileEmployee entity + * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE' + * @returns Object with status and details + */ + private async createKeycloakUserFromProfile( + profile: Profile | ProfileEmployee, + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise<{ + success: boolean; + action: "created" | "verified" | "skipped" | "error"; + keycloakUserId?: string; + error?: string; + }> { + try { + // Check if user already exists by username (citizenId) + const existingUserByUsername = await getUserByUsername(profile.citizenId); + if (Array.isArray(existingUserByUsername) && existingUserByUsername.length > 0) { + // User already exists with this username, update the keycloak field + const existingUserId = existingUserByUsername[0].id; + console.log( + `User with citizenId ${profile.citizenId} already exists in Keycloak with ID ${existingUserId}`, + ); + + // Update the keycloak field in database + if (profileType === "PROFILE") { + await this.profileRepo.update(profile.id, { keycloak: existingUserId }); + } else { + await this.profileEmployeeRepo.update(profile.id, { keycloak: existingUserId }); + } + + // Assign default USER role to existing user + const userRole = await getRoles("USER"); + if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { + const roleAssigned = await addUserRoles(existingUserId, [ + { id: String(userRole.id), name: String(userRole.name) }, + ]); + if (roleAssigned) { + console.log(`Assigned USER role to existing user ${existingUserId}`); + } else { + console.warn(`Failed to assign USER role to existing user ${existingUserId}`); + } + } else { + console.warn(`USER role not found in Keycloak`); + } + + return { + success: true, + action: "verified", + keycloakUserId: existingUserId, + }; + } + + // Create new user in Keycloak + const createResult = await createUser(profile.citizenId, "P@ssw0rd", { + firstName: profile.firstName || "", + lastName: profile.lastName || "", + email: profile.email || undefined, + enabled: true, + }); + + if (!createResult || typeof createResult !== "string") { + return { + success: false, + action: "error", + error: "Failed to create user in Keycloak", + }; + } + + const keycloakUserId = createResult; + + // Update the keycloak field in database + if (profileType === "PROFILE") { + await this.profileRepo.update(profile.id, { keycloak: keycloakUserId }); + } else { + await this.profileEmployeeRepo.update(profile.id, { keycloak: keycloakUserId }); + } + + // Assign default USER role + const userRole = await getRoles("USER"); + if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) { + const roleAssigned = await addUserRoles(keycloakUserId, [ + { id: String(userRole.id), name: String(userRole.name) }, + ]); + if (roleAssigned) { + console.log(`Assigned USER role to user ${keycloakUserId}`); + } else { + console.warn(`Failed to assign USER role to user ${keycloakUserId}`); + } + } else { + console.warn(`USER role not found in Keycloak`); + } + + console.log( + `Created Keycloak user for profile ${profile.id} (citizenId: ${profile.citizenId}) with ID ${keycloakUserId}`, + ); + + return { + success: true, + action: "created", + keycloakUserId, + }; + } catch (error: any) { + console.error(`Error creating Keycloak user for profile ${profile.id}:`, error); + return { + success: false, + action: "error", + error: error.message || "Unknown error", + }; + } + } + + /** + * Process items in parallel with concurrency limit + */ + private async processInParallel( + items: T[], + concurrencyLimit: number, + processor: (item: T, index: number) => Promise, + ): Promise> { + const results: Array = []; + + // Process items in batches + for (let i = 0; i < items.length; i += concurrencyLimit) { + const batch = items.slice(i, i + concurrencyLimit); + + // Process batch in parallel with error handling + const batchResults = await Promise.all( + batch.map(async (item, batchIndex) => { + try { + return await processor(item, i + batchIndex); + } catch (error) { + return { error }; + } + }), + ); + + results.push(...batchResults); + + // Log progress after each batch + const completed = Math.min(i + concurrencyLimit, items.length); + console.log(`Progress: ${completed}/${items.length}`); + } + + return results; + } + + /** + * Batch ensure Keycloak users for all profiles + * Processes all rows in Profile and ProfileEmployee tables + * + * @returns Object with total, success, failed counts and details + */ + async batchEnsureKeycloakUsers(): Promise<{ + total: number; + created: number; + verified: number; + skipped: number; + failed: number; + details: Array<{ + profileId: string; + profileType: string; + action: string; + keycloakUserId?: string; + error?: string; + }>; + }> { + const result = { + total: 0, + created: 0, + verified: 0, + skipped: 0, + failed: 0, + details: [] as Array<{ + profileId: string; + profileType: string; + action: string; + keycloakUserId?: string; + error?: string; + }>, + }; + + try { + // Get all profiles from Profile table (ข้าราชการ) + const profiles = await this.profileRepo.find({ where: { isLeave: false } }); // Only active profiles + + // Get all profiles from ProfileEmployee table (ลูกจ้าง) + const profileEmployees = await this.profileEmployeeRepo.find({ where: { isLeave: false } }); // Only active profiles + + const allProfiles = [ + ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })), + ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })), + ]; + + result.total = allProfiles.length; + + // Process in parallel with concurrency limit + const CONCURRENCY_LIMIT = 5; // Adjust based on environment + + const processedResults = await this.processInParallel( + allProfiles, + CONCURRENCY_LIMIT, + async ({ profile, type }) => { + const ensureResult = await this.ensureKeycloakUser(profile.id, type); + + // Update counters + switch (ensureResult.action) { + case "created": + result.created++; + break; + case "verified": + result.verified++; + break; + case "skipped": + result.skipped++; + break; + case "error": + result.failed++; + break; + } + + return { + profileId: profile.id, + profileType: type, + action: ensureResult.action, + keycloakUserId: ensureResult.keycloakUserId, + error: ensureResult.error, + }; + }, + ); + + // Separate results from errors + for (const resultItem of processedResults) { + if ("error" in resultItem) { + result.failed++; + result.details.push({ + profileId: "unknown", + profileType: "unknown", + action: "error", + error: JSON.stringify(resultItem.error), + }); + } else { + result.details.push(resultItem); + } + } + + console.log( + `Batch ensure Keycloak users completed: total=${result.total}, created=${result.created}, verified=${result.verified}, skipped=${result.skipped}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in batch ensure Keycloak users:", error); + } + + return result; + } + + /** + * Clear orphaned Keycloak users + * Deletes users in Keycloak that are not referenced in Profile or ProfileEmployee tables + * + * @param skipUsernames - Array of usernames to skip (e.g., ['super_admin']) + * @returns Object with counts and details + */ + async clearOrphanedKeycloakUsers(skipUsernames: string[] = []): Promise<{ + totalInKeycloak: number; + totalInDatabase: number; + orphanedCount: number; + deleted: number; + skipped: number; + failed: number; + details: Array<{ + keycloakUserId: string; + username: string; + action: "deleted" | "skipped" | "error"; + error?: string; + }>; + }> { + const result = { + totalInKeycloak: 0, + totalInDatabase: 0, + orphanedCount: 0, + deleted: 0, + skipped: 0, + failed: 0, + details: [] as Array<{ + keycloakUserId: string; + username: string; + action: "deleted" | "skipped" | "error"; + error?: string; + }>, + }; + + try { + // Get all keycloak IDs from database (Profile + ProfileEmployee) + const profiles = await this.profileRepo + .createQueryBuilder("p") + .where("p.keycloak IS NOT NULL") + .andWhere("p.keycloak != :empty", { empty: "" }) + .getMany(); + + const profileEmployees = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .where("pe.keycloak IS NOT NULL") + .andWhere("pe.keycloak != :empty", { empty: "" }) + .getMany(); + + // Create a Set of all keycloak IDs in database for O(1) lookup + const databaseKeycloakIds = new Set(); + for (const p of profiles) { + if (p.keycloak) databaseKeycloakIds.add(p.keycloak); + } + for (const pe of profileEmployees) { + if (pe.keycloak) databaseKeycloakIds.add(pe.keycloak); + } + + result.totalInDatabase = databaseKeycloakIds.size; + + // Get all users from Keycloak with pagination to avoid response size limits + const keycloakUsers = await getAllUsersPaginated(); + if (!keycloakUsers || typeof keycloakUsers !== "object") { + throw new Error("Failed to get users from Keycloak"); + } + + result.totalInKeycloak = keycloakUsers.length; + + // Find orphaned users (in Keycloak but not in database) + const orphanedUsers = keycloakUsers.filter((user: any) => !databaseKeycloakIds.has(user.id)); + result.orphanedCount = orphanedUsers.length; + + // Delete orphaned users (skip protected ones) + for (const user of orphanedUsers) { + const username = user.username; + const userId = user.id; + + // Check if user should be skipped + if (skipUsernames.includes(username)) { + result.skipped++; + result.details.push({ + keycloakUserId: userId, + username, + action: "skipped", + }); + continue; + } + + // Delete user from Keycloak + try { + const deleteSuccess = await deleteUser(userId); + if (deleteSuccess) { + result.deleted++; + result.details.push({ + keycloakUserId: userId, + username, + action: "deleted", + }); + } else { + result.failed++; + result.details.push({ + keycloakUserId: userId, + username, + action: "error", + error: "Failed to delete user from Keycloak", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ + keycloakUserId: userId, + username, + action: "error", + error: error.message || "Unknown error", + }); + } + } + + console.log( + `Clear orphaned Keycloak users completed: totalInKeycloak=${result.totalInKeycloak}, totalInDatabase=${result.totalInDatabase}, orphaned=${result.orphanedCount}, deleted=${result.deleted}, skipped=${result.skipped}, failed=${result.failed}`, + ); + } catch (error) { + console.error("Error in clear orphaned Keycloak users:", error); + throw error; + } + + return result; + } +} From 49a8494a8d5ade7f9ef3cebaa14ee843073b82cd Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Feb 2026 11:40:56 +0700 Subject: [PATCH 130/178] fix emp temp and add script clear org dna at keycloak --- src/controllers/KeycloakSyncController.ts | 42 +++ src/controllers/ScriptProfileOrgController.ts | 61 +++- src/services/KeycloakAttributeService.ts | 317 +++++++++++++++--- 3 files changed, 363 insertions(+), 57 deletions(-) diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts index 81dbbf66..f24eee35 100644 --- a/src/controllers/KeycloakSyncController.ts +++ b/src/controllers/KeycloakSyncController.ts @@ -187,6 +187,48 @@ export class KeycloakSyncController extends Controller { }); } + /** + * Clear org DNA attributes for profiles (Admin only) + * + * @summary Clear org DNA attributes in Keycloak for given profiles (ADMIN) + * + * @description + * This endpoint will: + * - Clear all org DNA fields (orgRootDnaId, orgChild1-4DnaId) by setting them to empty strings + * - Use when an employee leaves their position (current_holderId becomes null) + * + * @param {request} request Request body containing profileIds array and profileType + */ + @Post("clear-org-dna") + async clearOrgDna( + @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" }, + ) { + const { profileIds, profileType } = request; + + // Validate profileIds + if (!profileIds || profileIds.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า"); + } + + // Validate profileType + if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", + ); + } + + const result = await this.keycloakAttributeService.clearOrgDnaAttributes( + profileIds, + profileType, + ); + + return new HttpSuccess({ + message: "Clear org DNA attributes เสร็จสิ้น", + ...result, + }); + } + /** * Batch sync all users (Admin only) * diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts index c8975e43..aa6908e2 100644 --- a/src/controllers/ScriptProfileOrgController.ts +++ b/src/controllers/ScriptProfileOrgController.ts @@ -9,6 +9,7 @@ import { PosMaster } from "./../entities/PosMaster"; import axios from "axios"; import { KeycloakSyncController } from "./KeycloakSyncController"; import { EmployeePosMaster } from "./../entities/EmployeePosMaster"; +import { EmployeeTempPosMaster } from "./../entities/EmployeeTempPosMaster"; interface OrgUpdatePayload { profileId: string; @@ -26,6 +27,7 @@ interface OrgUpdatePayload { export class ScriptProfileOrgController extends Controller { private posMasterRepo = AppDataSource.getRepository(PosMaster); private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); + private employeeTempPosMasterRepo = AppDataSource.getRepository(EmployeeTempPosMaster); // Idempotency flag to prevent concurrent runs private isRunning = false; @@ -37,6 +39,11 @@ export class ScriptProfileOrgController extends Controller { 10, ); + /** + * Script to update profile's organizational structure in leave service and sync to Keycloak + * + * @summary Update org structure for profiles updated within a certain time window and sync to Keycloak + */ @Post("update-org") public async cronjobUpdateOrg(@Request() request: RequestWithUser) { // Idempotency check - prevent concurrent runs @@ -61,7 +68,7 @@ export class ScriptProfileOrgController extends Controller { }); // Query with optimized select - only fetch required fields - const [posMasters, posMasterEmployee] = await Promise.all([ + const [posMasters, posMasterEmployee, posMasterEmployeeTemp] = await Promise.all([ this.posMasterRepo.find({ where: { lastUpdatedAt: MoreThanOrEqual(windowStart), @@ -120,16 +127,46 @@ export class ScriptProfileOrgController extends Controller { current_holder: { id: true }, }, }), + this.employeeTempPosMasterRepo.find({ + where: { + lastUpdatedAt: MoreThanOrEqual(windowStart), + orgRevision: { + orgRevisionIsCurrent: true, + }, + }, + relations: [ + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], + select: { + id: true, + current_holderId: true, + lastUpdatedAt: true, + orgRevision: { id: true }, + orgRoot: { ancestorDNA: true }, + orgChild1: { ancestorDNA: true }, + orgChild2: { ancestorDNA: true }, + orgChild3: { ancestorDNA: true }, + orgChild4: { ancestorDNA: true }, + current_holder: { id: true }, + }, + }), ]); console.log("cronjobUpdateOrg: Database query completed", { posMastersCount: posMasters.length, employeePosCount: posMasterEmployee.length, - totalRecords: posMasters.length + posMasterEmployee.length, + employeeTempPosCount: posMasterEmployeeTemp.length, + totalRecords: posMasters.length + posMasterEmployee.length + posMasterEmployeeTemp.length, }); // Build payloads with proper profile type tracking - const payloads = this.buildPayloads(posMasters, posMasterEmployee); + const payloads = this.buildPayloads(posMasters, posMasterEmployee, posMasterEmployeeTemp); if (payloads.length === 0) { console.log("cronjobUpdateOrg: No records to process"); @@ -246,12 +283,13 @@ export class ScriptProfileOrgController extends Controller { } /** - * Build payloads from PosMaster and EmployeePosMaster records + * Build payloads from PosMaster, EmployeePosMaster, and EmployeeTempPosMaster records * Includes proper profile type tracking for accurate Keycloak sync */ private buildPayloads( posMasters: PosMaster[], posMasterEmployee: EmployeePosMaster[], + posMasterEmployeeTemp: EmployeeTempPosMaster[], ): OrgUpdatePayload[] { const payloads: OrgUpdatePayload[] = []; @@ -285,6 +323,21 @@ export class ScriptProfileOrgController extends Controller { } } + // Process EmployeeTempPosMaster records (PROFILE_EMPLOYEE type) + for (const employeeTempPos of posMasterEmployeeTemp) { + if (employeeTempPos.current_holder && employeeTempPos.current_holderId) { + payloads.push({ + profileId: employeeTempPos.current_holderId, + rootDnaId: employeeTempPos.orgRoot?.ancestorDNA || null, + child1DnaId: employeeTempPos.orgChild1?.ancestorDNA || null, + child2DnaId: employeeTempPos.orgChild2?.ancestorDNA || null, + child3DnaId: employeeTempPos.orgChild3?.ancestorDNA || null, + child4DnaId: employeeTempPos.orgChild4?.ancestorDNA || null, + profileType: "PROFILE_EMPLOYEE", + }); + } + } + return payloads; } diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index 5b61e286..fcc77247 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -88,40 +88,101 @@ export class KeycloakAttributeService { } // If not found in Profile, try ProfileEmployee (ลูกจ้าง) - const profileEmployeeResult = await this.profileEmployeeRepo + // First, get the profileEmployee to check employeeClass + const profileEmployeeBasic = await this.profileEmployeeRepo .createQueryBuilder("pe") - .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") - .leftJoinAndSelect("epm.orgChild1", "orgChild1") - .leftJoinAndSelect("epm.orgChild2", "orgChild2") - .leftJoinAndSelect("epm.orgChild3", "orgChild3") - .leftJoinAndSelect("epm.orgChild4", "orgChild4") .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) .getOne(); - if ( - profileEmployeeResult && - profileEmployeeResult.current_holders && - profileEmployeeResult.current_holders.length > 0 - ) { - const currentPos = profileEmployeeResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + if (!profileEmployeeBasic) { + // Return null values if no profile found return { - profileId: profileEmployeeResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: profileEmployeeResult.employeeClass, - prefix: profileEmployeeResult.prefix, + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + prefix: null, }; } + // Check employeeClass to determine which table to query + const isPermEmployee = profileEmployeeBasic.employeeClass === "PERM"; + + if (isPermEmployee) { + // ลูกจ้างประจำ (PERM) - ใช้ EmployeePosMaster + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("epm.orgChild1", "orgChild1") + .leftJoinAndSelect("epm.orgChild2", "orgChild2") + .leftJoinAndSelect("epm.orgChild3", "orgChild3") + .leftJoinAndSelect("epm.orgChild4", "orgChild4") + .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + } else { + // ลูกจ้างชั่วคราว (TEMP) - ใช้ EmployeeTempPosMaster + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holderTemps", "etpm") + .leftJoinAndSelect("etpm.orgRoot", "org") + .leftJoinAndSelect("etpm.orgChild1", "orgChild1") + .leftJoinAndSelect("etpm.orgChild2", "orgChild2") + .leftJoinAndSelect("etpm.orgChild3", "orgChild3") + .leftJoinAndSelect("etpm.orgChild4", "orgChild4") + .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holderTemps && + profileEmployeeResult.current_holderTemps.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holderTemps[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + } + // Return null values if no profile found return { profileId: null, @@ -187,40 +248,101 @@ export class KeycloakAttributeService { }; } } else { - const profileEmployeeResult = await this.profileEmployeeRepo + // First, get the profileEmployee to check employeeClass + const profileEmployeeBasic = await this.profileEmployeeRepo .createQueryBuilder("pe") - .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") - .leftJoinAndSelect("pm.orgChild1", "orgChild1") - .leftJoinAndSelect("pm.orgChild2", "orgChild2") - .leftJoinAndSelect("pm.orgChild3", "orgChild3") - .leftJoinAndSelect("pm.orgChild4", "orgChild4") .where("pe.id = :profileId", { profileId }) .getOne(); - if ( - profileEmployeeResult && - profileEmployeeResult.current_holders && - profileEmployeeResult.current_holders.length > 0 - ) { - const currentPos = profileEmployeeResult.current_holders[0]; - const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; - const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; - const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; - const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; - const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; - + if (!profileEmployeeBasic) { return { - profileId: profileEmployeeResult.id, - orgRootDnaId, - orgChild1DnaId, - orgChild2DnaId, - orgChild3DnaId, - orgChild4DnaId, - empType: profileEmployeeResult.employeeClass, - prefix: profileEmployeeResult.prefix, + profileId: null, + orgRootDnaId: null, + orgChild1DnaId: null, + orgChild2DnaId: null, + orgChild3DnaId: null, + orgChild4DnaId: null, + empType: null, + prefix: null, }; } + + // Check employeeClass to determine which table to query + const isPermEmployee = profileEmployeeBasic.employeeClass === "PERM"; + + if (isPermEmployee) { + // ลูกจ้างประจำ (PERM) - ใช้ EmployeePosMaster + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holders", "epm") + .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("epm.orgChild1", "orgChild1") + .leftJoinAndSelect("epm.orgChild2", "orgChild2") + .leftJoinAndSelect("epm.orgChild3", "orgChild3") + .leftJoinAndSelect("epm.orgChild4", "orgChild4") + .where("pe.id = :profileId", { profileId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holders && + profileEmployeeResult.current_holders.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holders[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + } else { + // ลูกจ้างชั่วคราว (TEMP) - ใช้ EmployeeTempPosMaster + const profileEmployeeResult = await this.profileEmployeeRepo + .createQueryBuilder("pe") + .leftJoinAndSelect("pe.current_holderTemps", "etpm") + .leftJoinAndSelect("etpm.orgRoot", "org") + .leftJoinAndSelect("etpm.orgChild1", "orgChild1") + .leftJoinAndSelect("etpm.orgChild2", "orgChild2") + .leftJoinAndSelect("etpm.orgChild3", "orgChild3") + .leftJoinAndSelect("etpm.orgChild4", "orgChild4") + .where("pe.id = :profileId", { profileId }) + .getOne(); + + if ( + profileEmployeeResult && + profileEmployeeResult.current_holderTemps && + profileEmployeeResult.current_holderTemps.length > 0 + ) { + const currentPos = profileEmployeeResult.current_holderTemps[0]; + const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || ""; + const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || ""; + const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || ""; + const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || ""; + const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || ""; + + return { + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, + }; + } + } } return { @@ -313,6 +435,95 @@ export class KeycloakAttributeService { } } + /** + * Clear org DNA attributes in Keycloak for given profiles + * Sets all org DNA fields to empty strings + * + * @param profileIds - Array of profile IDs to clear + * @param profileType - 'PROFILE' for officers or 'PROFILE_EMPLOYEE' for employees + * @returns Object with success/failed counts and details + */ + async clearOrgDnaAttributes( + profileIds: string[], + profileType: "PROFILE" | "PROFILE_EMPLOYEE", + ): Promise<{ + total: number; + success: number; + failed: number; + details: Array<{ profileId: string; status: "success" | "failed"; error?: string }>; + }> { + const result = { + total: profileIds.length, + success: 0, + failed: 0, + details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>, + }; + + for (const profileId of profileIds) { + try { + // Get the keycloak userId from the profile + let keycloakUserId: string | null = null; + + if (profileType === "PROFILE") { + const profile = await this.profileRepo.findOne({ where: { id: profileId } }); + keycloakUserId = profile?.keycloak || ""; + } else { + const profileEmployee = await this.profileEmployeeRepo.findOne({ + where: { id: profileId }, + }); + keycloakUserId = profileEmployee?.keycloak || ""; + } + + if (!keycloakUserId) { + result.failed++; + result.details.push({ + profileId, + status: "failed", + error: "No Keycloak user ID found", + }); + continue; + } + + // Clear org DNA attributes by setting them to empty strings + const clearedAttributes: Record = { + orgRootDnaId: [""], + orgChild1DnaId: [""], + orgChild2DnaId: [""], + orgChild3DnaId: [""], + orgChild4DnaId: [""], + }; + + const success = await updateUserAttributes(keycloakUserId, clearedAttributes); + + if (success) { + result.success++; + result.details.push({ + profileId, + status: "success", + }); + console.log(`Cleared org DNA attributes for profile ${profileId} (${profileType})`); + } else { + result.failed++; + result.details.push({ + profileId, + status: "failed", + error: "Failed to update Keycloak attributes", + }); + } + } catch (error: any) { + result.failed++; + result.details.push({ + profileId, + status: "failed", + error: error.message || "Unknown error", + }); + console.error(`Error clearing org DNA attributes for profile ${profileId}:`, error); + } + } + + return result; + } + /** * Batch sync multiple users with unlimited count and parallel processing * Useful for initial sync or periodic updates From e4f46a17626c3f6a2b823ee678823db782821efb Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Feb 2026 11:57:27 +0700 Subject: [PATCH 131/178] fix condition org revision current id of perm and term --- src/services/KeycloakAttributeService.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index fcc77247..c5759274 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -116,12 +116,13 @@ export class KeycloakAttributeService { const profileEmployeeResult = await this.profileEmployeeRepo .createQueryBuilder("pe") .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("epm.orgRoot", "orgRoot") .leftJoinAndSelect("epm.orgChild1", "orgChild1") .leftJoinAndSelect("epm.orgChild2", "orgChild2") .leftJoinAndSelect("epm.orgChild3", "orgChild3") .leftJoinAndSelect("epm.orgChild4", "orgChild4") .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) .getOne(); if ( @@ -151,12 +152,13 @@ export class KeycloakAttributeService { const profileEmployeeResult = await this.profileEmployeeRepo .createQueryBuilder("pe") .leftJoinAndSelect("pe.current_holderTemps", "etpm") - .leftJoinAndSelect("etpm.orgRoot", "org") + .leftJoinAndSelect("etpm.orgRoot", "orgRoot") .leftJoinAndSelect("etpm.orgChild1", "orgChild1") .leftJoinAndSelect("etpm.orgChild2", "orgChild2") .leftJoinAndSelect("etpm.orgChild3", "orgChild3") .leftJoinAndSelect("etpm.orgChild4", "orgChild4") .where("pe.keycloak = :keycloakUserId", { keycloakUserId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) .getOne(); if ( @@ -275,12 +277,13 @@ export class KeycloakAttributeService { const profileEmployeeResult = await this.profileEmployeeRepo .createQueryBuilder("pe") .leftJoinAndSelect("pe.current_holders", "epm") - .leftJoinAndSelect("epm.orgRoot", "org") + .leftJoinAndSelect("epm.orgRoot", "orgRoot") .leftJoinAndSelect("epm.orgChild1", "orgChild1") .leftJoinAndSelect("epm.orgChild2", "orgChild2") .leftJoinAndSelect("epm.orgChild3", "orgChild3") .leftJoinAndSelect("epm.orgChild4", "orgChild4") .where("pe.id = :profileId", { profileId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) .getOne(); if ( @@ -311,12 +314,13 @@ export class KeycloakAttributeService { const profileEmployeeResult = await this.profileEmployeeRepo .createQueryBuilder("pe") .leftJoinAndSelect("pe.current_holderTemps", "etpm") - .leftJoinAndSelect("etpm.orgRoot", "org") + .leftJoinAndSelect("etpm.orgRoot", "orgRoot") .leftJoinAndSelect("etpm.orgChild1", "orgChild1") .leftJoinAndSelect("etpm.orgChild2", "orgChild2") .leftJoinAndSelect("etpm.orgChild3", "orgChild3") .leftJoinAndSelect("etpm.orgChild4", "orgChild4") .where("pe.id = :profileId", { profileId }) + .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId }) .getOne(); if ( From 2951630b7b61b0399e71dae158837cb872e5cf06 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Feb 2026 15:24:51 +0700 Subject: [PATCH 132/178] fix update prefix and profileId --- src/controllers/CommandController.ts | 21 ++++++-- src/controllers/MyController.ts | 3 +- src/controllers/OrganizationController.ts | 49 ++++++++++++++----- .../ProfileChangeNameController.ts | 19 ++++--- .../ProfileChangeNameEmployeeController.ts | 18 ++++--- ...ProfileChangeNameEmployeeTempController.ts | 12 +++-- src/controllers/ProfileController.ts | 34 ++++++++----- src/controllers/UserController.ts | 49 ++++++++++++++----- src/keycloak/index.ts | 45 +++++++++++++++++ src/services/KeycloakAttributeService.ts | 20 +++++--- 10 files changed, 201 insertions(+), 69 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index aa2ab3d1..857d6d55 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -64,6 +64,7 @@ import { getRoleMappings, removeUserRoles, getToken, + updateUserAttributes, } from "../keycloak"; import { ProfileEducation, CreateProfileEducation } from "../entities/ProfileEducation"; import { ProfileEducationHistory } from "../entities/ProfileEducationHistory"; @@ -4242,6 +4243,12 @@ export class CommandController extends Controller { profile.isActive = true; } await this.profileRepository.save(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"].includes(code)) { let organizeName = ""; @@ -6329,8 +6336,7 @@ export class CommandController extends Controller { name: x.name, })), ); - } - else { + } else { userKeycloakId = checkUser[0].id; const rolesData = await getRoleMappings(userKeycloakId); if (rolesData) { @@ -6393,9 +6399,9 @@ export class CommandController extends Controller { if (profileEmployee.keycloak != null) { // const delUserKeycloak = await deleteUser(profileEmployee.keycloak); // if (delUserKeycloak) { - profileEmployee.keycloak = _null; - profileEmployee.roleKeycloaks = []; - profileEmployee.isActive = false; + profileEmployee.keycloak = _null; + profileEmployee.roleKeycloaks = []; + profileEmployee.isActive = false; // } } profileEmployee.isLeave = true; @@ -6448,6 +6454,11 @@ export class CommandController extends Controller { profile.phone = item.bodyProfile.phone ?? null; await this.profileRepository.save(profile); + // update user attribute in keycloak + await updateUserAttributes(profile.keycloak ?? "", { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); setLogDataDiff(req, { before, after: profile }); } //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ diff --git a/src/controllers/MyController.ts b/src/controllers/MyController.ts index f8fbc848..16809b6d 100644 --- a/src/controllers/MyController.ts +++ b/src/controllers/MyController.ts @@ -1,4 +1,3 @@ -import { profile } from "console"; import { Controller, Get, Post, Query, Route, Security, Tags } from "tsoa"; import { calculateGovAge } from "../interfaces/utils"; import HttpSuccess from "../interfaces/http-success"; @@ -14,7 +13,7 @@ export class AppController extends Controller { @Post() public async Post(@Query() profileId: string) { - const result = calculateGovAge(profileId,"OFFICER"); + const result = calculateGovAge(profileId, "OFFICER"); return new HttpSuccess(result); } } diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 89015402..fdf24b66 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -50,8 +50,13 @@ import { getUserByUsername, getRoles, addUserRoles, + updateUserAttributes, } from "../keycloak"; -import { getPositionCountsAggregated, getPositionCount, PositionCountsByNode } from "../services/OrganizationService"; +import { + getPositionCountsAggregated, + getPositionCount, + PositionCountsByNode, +} from "../services/OrganizationService"; import { BatchSavePosMasterHistoryOfficer, CreatePosMasterHistoryEmployee, @@ -4688,7 +4693,12 @@ export class OrganizationController extends Controller { case 2: { const data = await this.child1Repository.findOne({ where: { id: idNode }, - relations: ["orgRevision", "orgChild2s", "orgChild2s.orgChild3s", "orgChild2s.orgChild3s.orgChild4s"], + relations: [ + "orgRevision", + "orgChild2s", + "orgChild2s.orgChild3s", + "orgChild2s.orgChild3s.orgChild4s", + ], }); if (!data) { throw new HttpError(HttpStatusCode.NOT_FOUND, "not found child1Id"); @@ -4700,7 +4710,9 @@ export class OrganizationController extends Controller { deptID: data.id, type: 2, totalPositionCount: child1Counts.totalCount, - totalPositionVacant: isDraft ? child1Counts.nextVacantCount : child1Counts.currentVacantCount, + totalPositionVacant: isDraft + ? child1Counts.nextVacantCount + : child1Counts.currentVacantCount, children: this.buildOrgChild2s(data.orgChild2s, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); @@ -4720,7 +4732,9 @@ export class OrganizationController extends Controller { deptID: data.id, type: 3, totalPositionCount: child2Counts.totalCount, - totalPositionVacant: isDraft ? child2Counts.nextVacantCount : child2Counts.currentVacantCount, + totalPositionVacant: isDraft + ? child2Counts.nextVacantCount + : child2Counts.currentVacantCount, children: this.buildOrgChild3s(data.orgChild3s, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); @@ -4740,7 +4754,9 @@ export class OrganizationController extends Controller { deptID: data.id, type: 4, totalPositionCount: child3Counts.totalCount, - totalPositionVacant: isDraft ? child3Counts.nextVacantCount : child3Counts.currentVacantCount, + totalPositionVacant: isDraft + ? child3Counts.nextVacantCount + : child3Counts.currentVacantCount, children: this.buildOrgChild4s(data.orgChild4s, positionCounts, isDraft), }; return new HttpSuccess([formattedData]); @@ -4760,7 +4776,9 @@ export class OrganizationController extends Controller { deptID: data.id, type: 5, totalPositionCount: child4Counts.totalCount, - totalPositionVacant: isDraft ? child4Counts.nextVacantCount : child4Counts.currentVacantCount, + totalPositionVacant: isDraft + ? child4Counts.nextVacantCount + : child4Counts.currentVacantCount, }; return new HttpSuccess([formattedData]); } @@ -8089,7 +8107,12 @@ export class OrganizationController extends Controller { if (_item) { _item.roleKeycloaks = Array.from(new Set([..._item.roleKeycloaks, ...roleKeycloak])); check += 1; - await this.profileEmployeeRepo.save(_item); + _item = await this.profileEmployeeRepo.save(_item); + // update user attribute in keycloak + await updateUserAttributes(_item.keycloak, { + profileId: [_item.id], + prefix: [_item.prefix || ""], + }); } } catch (error) { console.error(`Error processing ${_item.citizenId}:`, error); @@ -9096,7 +9119,7 @@ export class OrganizationController extends Controller { */ private sumAllVacantCounts( map: Map, - isDraft: boolean + isDraft: boolean, ): number { let sum = 0; for (const value of map.values()) { @@ -9111,7 +9134,7 @@ export class OrganizationController extends Controller { private buildOrgRoots( orgRoots: OrgRoot[], positionCounts: PositionCountsByNode, - isDraft: boolean + isDraft: boolean, ) { if (!orgRoots) return []; return orgRoots @@ -9135,7 +9158,7 @@ export class OrganizationController extends Controller { private buildOrgChild1s( orgChild1s: OrgChild1[], positionCounts: PositionCountsByNode, - isDraft: boolean + isDraft: boolean, ) { if (!orgChild1s) return []; return orgChild1s @@ -9159,7 +9182,7 @@ export class OrganizationController extends Controller { private buildOrgChild2s( orgChild2s: OrgChild2[], positionCounts: PositionCountsByNode, - isDraft: boolean + isDraft: boolean, ) { if (!orgChild2s) return []; return orgChild2s @@ -9183,7 +9206,7 @@ export class OrganizationController extends Controller { private buildOrgChild3s( orgChild3s: OrgChild3[], positionCounts: PositionCountsByNode, - isDraft: boolean + isDraft: boolean, ) { if (!orgChild3s) return []; return orgChild3s @@ -9207,7 +9230,7 @@ export class OrganizationController extends Controller { private buildOrgChild4s( orgChild4s: OrgChild4[], positionCounts: PositionCountsByNode, - isDraft: boolean + isDraft: boolean, ) { if (!orgChild4s) return []; return orgChild4s diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 26741c46..77e63aa6 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -116,7 +116,12 @@ export class ProfileChangeNameController extends Controller { setLogDataDiff(req, { before, after: profile }); if (profile != null && profile.keycloak != null) { - const result = await updateName(profile.keycloak, profile.firstName, profile.lastName); + const result = await updateName( + profile.keycloak, + profile.firstName, + profile.lastName, + profile.prefix, + ); if (!result) { throw new Error(result.errorMessage); } @@ -182,7 +187,12 @@ export class ProfileChangeNameController extends Controller { // ปิดไว้ก่อนเพราะ error ต้องใช้ keycloak ที่มีสิทธิ์ในการ update //update 17/07 if (profile != null && profile.keycloak != null) { - const result = await updateName(profile.keycloak, profile.firstName, profile.lastName); + const result = await updateName( + profile.keycloak, + profile.firstName, + profile.lastName, + profile.prefix, + ); if (!result) { throw new Error(result.errorMessage); } @@ -197,10 +207,7 @@ export class ProfileChangeNameController extends Controller { * @param trainingId คีย์ประวัติการเปลี่ยนชื่อ - นามสกุล */ @Patch("update-delete/{changeNameId}") - public async updateIsDeleted( - @Request() req: RequestWithUser, - @Path() changeNameId: string, - ) { + public async updateIsDeleted(@Request() req: RequestWithUser, @Path() changeNameId: string) { const record = await this.changeNameRepository.findOneBy({ id: changeNameId }); if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); if (record.isDeleted === true) { diff --git a/src/controllers/ProfileChangeNameEmployeeController.ts b/src/controllers/ProfileChangeNameEmployeeController.ts index 7772646d..76f5da7f 100644 --- a/src/controllers/ProfileChangeNameEmployeeController.ts +++ b/src/controllers/ProfileChangeNameEmployeeController.ts @@ -122,7 +122,12 @@ export class ProfileChangeNameEmployeeController extends Controller { setLogDataDiff(req, { before, after: profile }); if (profile != null && profile.keycloak != null) { - const result = await updateName(profile.keycloak, profile.firstName, profile.lastName); + const result = await updateName( + profile.keycloak, + profile.firstName, + profile.lastName, + profile.prefix, + ); if (!result) { throw new Error(result.errorMessage); } @@ -195,16 +200,17 @@ export class ProfileChangeNameEmployeeController extends Controller { * @param trainingId คีย์ประวัติการเปลี่ยนชื่อ - นามสกุล */ @Patch("update-delete/{changeNameId}") - public async updateIsDeleted( - @Request() req: RequestWithUser, - @Path() changeNameId: string, - ) { + public async updateIsDeleted(@Request() req: RequestWithUser, @Path() changeNameId: string) { const record = await this.changeNameRepository.findOneBy({ id: changeNameId }); if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); if (record.isDeleted === true) { return new HttpSuccess(); } - await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + await new permission().PermissionOrgUserDelete( + req, + "SYS_REGISTRY_EMP", + record.profileEmployeeId, + ); const before = structuredClone(record); const history = new ProfileChangeNameHistory(); const now = new Date(); diff --git a/src/controllers/ProfileChangeNameEmployeeTempController.ts b/src/controllers/ProfileChangeNameEmployeeTempController.ts index 049e2b07..78aaa09d 100644 --- a/src/controllers/ProfileChangeNameEmployeeTempController.ts +++ b/src/controllers/ProfileChangeNameEmployeeTempController.ts @@ -114,7 +114,12 @@ export class ProfileChangeNameEmployeeTempController extends Controller { setLogDataDiff(req, { before, after: profile }); if (profile != null && profile.keycloak != null) { - const result = await updateName(profile.keycloak, profile.firstName, profile.lastName); + const result = await updateName( + profile.keycloak, + profile.firstName, + profile.lastName, + profile.prefix, + ); if (!result) { throw new Error(result.errorMessage); } @@ -187,10 +192,7 @@ export class ProfileChangeNameEmployeeTempController extends Controller { * @param trainingId คีย์ประวัติการเปลี่ยนชื่อ - นามสกุล */ @Patch("update-delete/{changeNameId}") - public async updateIsDeleted( - @Request() req: RequestWithUser, - @Path() changeNameId: string, - ) { + public async updateIsDeleted(@Request() req: RequestWithUser, @Path() changeNameId: string) { const record = await this.changeNameRepository.findOneBy({ id: changeNameId }); if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); if (record.isDeleted === true) { diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 483c580d..5fbfa83b 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -5092,7 +5092,12 @@ export class ProfileController extends Controller { // setLogDataDiff(request, { before, after: record }); if (record != null && record.keycloak != null) { - const result = await updateName(record.keycloak, record.firstName, record.lastName); + const result = await updateName( + record.keycloak, + record.firstName, + record.lastName, + record.prefix, + ); if (!result) { throw new Error(result.errorMessage); } @@ -5406,7 +5411,12 @@ export class ProfileController extends Controller { setLogDataDiff(request, { before, after: record }); if (record != null && record.keycloak != null) { - const result = await updateName(record.keycloak, record.firstName, record.lastName); + const result = await updateName( + record.keycloak, + record.firstName, + record.lastName, + record.prefix, + ); if (!result) { throw new Error(result.errorMessage); } @@ -5507,27 +5517,27 @@ export class ProfileController extends Controller { @Get("history/user") async getHistoryProfileByUser(@Request() request: RequestWithUser) { const profile = await this.profileRepo.findOne({ - where: { keycloak: request.user.sub } + where: { keycloak: request.user.sub }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const profileHistory = await this.profileHistoryRepo.find({ where: { profileId: profile.id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); - + if (profileHistory.length == 0) { await this.profileHistoryRepo.save( Object.assign(new ProfileHistory(), { ...profile, birthDateOld: profile?.birthDate, profileId: profile.id, - id: undefined + id: undefined, }), ); const firstRecord = await this.profileHistoryRepo.find({ where: { profileId: profile.id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); return new HttpSuccess(firstRecord); } @@ -6482,27 +6492,27 @@ export class ProfileController extends Controller { async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) { //await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", id); //ไม่แน่ใจOFFปิดไว้ก่อน const profile = await this.profileRepo.findOne({ - where: { id: id } + where: { id: id }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const profileHistory = await this.profileHistoryRepo.find({ where: { profileId: id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); - + if (profileHistory.length == 0) { await this.profileHistoryRepo.save( Object.assign(new ProfileHistory(), { ...profile, birthDateOld: profile?.birthDate, profileId: id, - id: undefined + id: undefined, }), ); const firstRecord = await this.profileHistoryRepo.find({ where: { profileId: id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); return new HttpSuccess(firstRecord); } diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index ecfef0ca..9889bd9b 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -33,6 +33,7 @@ import { getUserByUsername, changeUserPassword, resetPassword, + updateUserAttributes, } from "../keycloak"; import { AppDataSource } from "../database/data-source"; import { Profile } from "../entities/Profile"; @@ -137,6 +138,13 @@ export class KeycloakController extends Controller { } profile.email = body.email == null ? _null : body.email; await this.profileRepo.save(profile); + + // Update Keycloak with profile prefix after profile is loaded + await updateUserAttributes(userId, { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + if (body.roles != null && body.roles.length > 0) { const roleKeycloak = await this.roleKeycloakRepo.find({ where: { id: In(body.roles) }, @@ -195,6 +203,12 @@ export class KeycloakController extends Controller { } profile.email = body.email == null ? _null : body.email; await this.profileEmpRepo.save(profile); + // Update Keycloak with profile prefix after profile is loaded + await updateUserAttributes(userId, { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + if (body.roles != null && body.roles.length > 0) { const roleKeycloak = await this.roleKeycloakRepo.find({ where: { id: In(body.roles) }, @@ -475,14 +489,14 @@ export class KeycloakController extends Controller { @Request() req: RequestWithUser, @Body() body: { - page: number, - pageSize: number, - keyword: string | null, - type: string, - isAll: boolean, - node: number | null, - nodeId: string | null, - } + page: number; + pageSize: number; + keyword: string | null; + type: string; + isAll: boolean; + node: number | null; + nodeId: string | null; + }, ) { let checkChildFromRole: any = {}; @@ -558,10 +572,14 @@ export class KeycloakController extends Controller { .andWhere( new Brackets((qb) => { qb.orWhere( - body.keyword != null && body.keyword != "" ? `profile.citizenId like '%${body.keyword}%'` : "1=1", + body.keyword != null && body.keyword != "" + ? `profile.citizenId like '%${body.keyword}%'` + : "1=1", ) .orWhere( - body.keyword != null && body.keyword != "" ? `profile.email like '%${body.keyword}%'` : "1=1", + body.keyword != null && body.keyword != "" + ? `profile.email like '%${body.keyword}%'` + : "1=1", ) .orWhere( body.keyword != null && body.keyword != "" @@ -737,6 +755,12 @@ export class KeycloakController extends Controller { } profile.email = body.email == null ? _null : body.email; await this.profileEmpRepo.save(profile); + // Update Keycloak with profile prefix after profile is loaded + await updateUserAttributes(userId, { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + if (body.roles != null && body.roles.length > 0) { const roleKeycloak = await this.roleKeycloakRepo.find({ where: { id: In(body.roles) }, @@ -783,7 +807,6 @@ export class KeycloakController extends Controller { @Get("user/role/{id}") async getRoleUser(@Request() req: RequestWithUser, @Path("id") id: string) { - const profile = await this.profileRepo.findOne({ where: { keycloak: id }, relations: ["roleKeycloaks"], @@ -791,8 +814,8 @@ export class KeycloakController extends Controller { if ( req.user.sub === id && - req.user.role.some(x => x === 'ADMIN') && - !req.user.role.some(x => x === 'SUPER_ADMIN') + req.user.role.some((x) => x === "ADMIN") && + !req.user.role.some((x) => x === "SUPER_ADMIN") ) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่มีสิทธิ์เข้าถึงข้อมูลนี้"); } diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index 85724eec..90ab019c 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -298,6 +298,7 @@ export async function updateName( userId: string, firstName: string, lastName: string, + prefix: string, // opts: Record, ) { // const { password, ...rest } = opts; @@ -315,6 +316,9 @@ export async function updateName( // ...rest, firstName, lastName, + attributes: { + prefix, + }, }), }).catch((e) => console.log("Keycloak Error: ", e)); @@ -971,3 +975,44 @@ export async function getAllUsersPaginated( enabled: v.enabled === true || v.enabled === "true", })); } + +/** + * Create keycloak user by given username and password with roles + * + * Client must have permission to manage realm's user + * + * @returns user uuid or true if success, false otherwise. + */ +export async function createUserHaveProfile( + username: string, + password: string, + profileId: string, + prefix: string, + opts?: Record, + token?: string, +) { + const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, { + // prettier-ignore + headers: { + "authorization": `Bearer ${token || await getToken()}`, + "content-type": `application/json`, + }, + method: "POST", + body: JSON.stringify({ + enabled: true, + credentials: [{ type: "password", value: password, temporary: false }], + username, + ...opts, + }), + }).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(); + } + + const path = res.headers.get("Location"); + const id = path?.split("/").at(-1); + return id || true; +} diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index c5759274..53cce4b4 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -5,7 +5,7 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; // import { EmployeePosMaster } from "../entities/EmployeePosMaster"; // import { OrgRoot } from "../entities/OrgRoot"; import { - createUser, + createUserHaveProfile, getUser, getUserByUsername, updateUserAttributes, @@ -809,12 +809,18 @@ export class KeycloakAttributeService { } // Create new user in Keycloak - const createResult = await createUser(profile.citizenId, "P@ssw0rd", { - firstName: profile.firstName || "", - lastName: profile.lastName || "", - email: profile.email || undefined, - enabled: true, - }); + const createResult = await createUserHaveProfile( + profile.citizenId, + "P@ssw0rd", + profile.id, + profile.prefix, + { + firstName: profile.firstName || "", + lastName: profile.lastName || "", + email: profile.email || undefined, + enabled: true, + }, + ); if (!createResult || typeof createResult !== "string") { return { From c7c2f3a6c266a20fe8039ffcc132a23e4ddcfe95 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 4 Mar 2026 14:39:02 +0700 Subject: [PATCH 133/178] =?UTF-8?q?=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=81?= =?UTF-8?q?=E0=B8=B2=E0=B8=A3=E0=B8=AD=E0=B8=AD=E0=B8=81=E0=B8=84=E0=B8=B3?= =?UTF-8?q?=E0=B8=AA=E0=B8=B1=E0=B9=88=E0=B8=87=20owner=20=E0=B9=80?= =?UTF-8?q?=E0=B8=AB=E0=B9=87=E0=B8=99=E0=B8=AB=E0=B8=A1=E0=B8=94=20=20#15?= =?UTF-8?q?51=20&=20Fix=20Bug=20=E0=B8=A5=E0=B8=B9=E0=B8=81=E0=B8=88?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B8=88?= =?UTF-8?q?=E0=B8=B3=E0=B8=AA=E0=B8=A1=E0=B8=B1=E0=B8=84=E0=B8=A3=E0=B8=AA?= =?UTF-8?q?=E0=B8=AD=E0=B8=9A=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=20?= =?UTF-8?q?=E0=B8=82=E0=B8=A3=E0=B8=81.=20#2343?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 31 ++++++++++++++++------------ 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 857d6d55..a62ddadc 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -187,17 +187,27 @@ export class CommandController extends Controller { (x) => x.orgRevision?.orgRevisionIsCurrent == true && x.orgRevision?.orgRevisionIsDraft == false, )[0]?.isDirector || false; - if (isDirector) { - let _data: any = { + let _data: any = { root: null, child1: null, child2: null, child3: null, child4: null, }; - if (!request.user.role.includes("SUPER_ADMIN")) { - _data = await new permission().PermissionOrgList(request, "COMMAND"); - } + if (!request.user.role.includes("SUPER_ADMIN")) { + _data = await new permission().PermissionOrgList(request, "COMMAND"); + } + if (isDirector || _data.privilege == "OWNER") { + // let _data: any = { + // root: null, + // child1: null, + // child2: null, + // child3: null, + // child4: null, + // }; + // if (!request.user.role.includes("SUPER_ADMIN")) { + // _data = await new permission().PermissionOrgList(request, "COMMAND"); + // } const profiles = await this.profileRepository .createQueryBuilder("profile") .leftJoinAndSelect("profile.current_holders", "current_holders") @@ -6208,7 +6218,6 @@ export class CommandController extends Controller { }); const list = await getRoles(); if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); - const _null: any = null; let _posNumCodeSit: string = ""; let _posNumCodeSitAbb: string = ""; const _command = await this.commandRepository.findOne({ @@ -6362,6 +6371,7 @@ export class CommandController extends Controller { relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], }); let _oldInsigniaIds: string[] = []; + //ลูกจ้างประจำ หรือ บุคคลภายนอก if (!profile) { //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม let profileEmployee: any = await this.profileEmployeeRepository.findOne({ @@ -6393,7 +6403,7 @@ export class CommandController extends Controller { await this.salaryHistoryRepo.save(history, { data: req }); if (profileEmployee.profileInsignias.length > 0) { - _oldInsigniaIds = profileEmployee.profileInsignias.filter().map((x: any) => x.id); + _oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? []; } await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE"); if (profileEmployee.keycloak != null) { @@ -6469,7 +6479,7 @@ export class CommandController extends Controller { ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) ) { if (profile.profileInsignias.length > 0) { - _oldInsigniaIds = profile.profileInsignias.map((x: any) => x.id); + _oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? []; } profile = Object.assign({ ...item.bodyProfile, ...meta }); profile.dateRetire = _dateRetire; @@ -6627,7 +6637,6 @@ export class CommandController extends Controller { }), ); } - //Certificates if (item.bodyCertificates && item.bodyCertificates.length > 0) { await Promise.all( @@ -6644,7 +6653,6 @@ export class CommandController extends Controller { }), ); } - //FamilyCouple if (item.bodyMarry != null) { const profileCouple = new ProfileFamilyCouple(); @@ -6666,7 +6674,6 @@ export class CommandController extends Controller { coupleHistory.profileFamilyCoupleId = profileCouple.id; await this.profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req }); } - //FamilyFather if (item.bodyFather != null) { const profileFather = new ProfileFamilyFather(); @@ -6687,7 +6694,6 @@ export class CommandController extends Controller { fatherHistory.profileFamilyFatherId = profileFather.id; await this.profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req }); } - //FamilyMother if (item.bodyMother != null) { const profileMother = new ProfileFamilyMother(); @@ -6708,7 +6714,6 @@ export class CommandController extends Controller { motherHistory.profileFamilyMotherId = profileMother.id; await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); } - //Salary if (item.bodySalarys && item.bodySalarys != null) { const dest_item = await this.salaryRepo.findOne({ From e50b95e9bd8d21214d8f2ac961cb3929eb76d40b Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 4 Mar 2026 16:53:09 +0700 Subject: [PATCH 134/178] fix build from tag --- .forgejo/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/ci-cd.yml b/.forgejo/workflows/ci-cd.yml index 0f913ddc..92472637 100644 --- a/.forgejo/workflows/ci-cd.yml +++ b/.forgejo/workflows/ci-cd.yml @@ -3,8 +3,8 @@ name: Build & Deploy on Dev on: push: - branches: - - dev + tags: + - "v[0-9]+.[0-9]+.[0-9]+" workflow_dispatch: env: From 0ecd3541524d1a10d0e3c86b68fe829f4be8b8ef Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 4 Mar 2026 16:55:07 +0700 Subject: [PATCH 135/178] remove action on build tag --- .forgejo/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index ba3556e0..37c4a2db 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -1,11 +1,11 @@ name: Build -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+*" - workflow_dispatch: +# on: +# push: +# tags: +# - "v[0-9]+.[0-9]+.[0-9]+" +# - "v[0-9]+.[0-9]+.[0-9]+*" +# workflow_dispatch: env: REGISTRY: ${{ vars.CONTAINER_REGISTRY }} From f59a5eec8086d8a8a8a96e2a3424c63ff0da2172 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 4 Mar 2026 17:02:19 +0700 Subject: [PATCH 136/178] fix tag version --- .forgejo/workflows/ci-cd.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/ci-cd.yml b/.forgejo/workflows/ci-cd.yml index 92472637..9b2545e9 100644 --- a/.forgejo/workflows/ci-cd.yml +++ b/.forgejo/workflows/ci-cd.yml @@ -29,7 +29,11 @@ jobs: ca=["/etc/ssl/certs/ca-certificates.crt"] - name: Tag Version run: | - echo "IMAGE_VERSION=latest" + if [ "${{ github.ref_type }}" == "tag" ]; then + echo "IMAGE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + else + echo "IMAGE_VERSION=latest" >> $GITHUB_ENV + fi - name: Login in to registry uses: docker/login-action@v2 with: From f8bb9e7cab6d90cc5f88ce054cbadf1c9e075ee4 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 4 Mar 2026 17:41:37 +0700 Subject: [PATCH 137/178] =?UTF-8?q?=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=E0=B8=9F=E0=B8=B4=E0=B8=A5=E0=B8=94=E0=B9=8C=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B8=97=E0=B8=B3=E0=B9=83=E0=B8=AB=E0=B9=89?= =?UTF-8?q?=E0=B8=A3=E0=B8=B9=E0=B9=89=E0=B8=A7=E0=B9=88=E0=B8=B2=E0=B8=81?= =?UTF-8?q?=E0=B8=B2=E0=B8=A3=E0=B8=A3=E0=B9=89=E0=B8=AD=E0=B8=87=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=E0=B8=B5=E0=B9=89=E0=B8=A1=E0=B8=B2=E0=B8=88?= =?UTF-8?q?=E0=B8=B2=E0=B8=81=E0=B8=AA=E0=B8=B3=E0=B8=99=E0=B8=B1=E0=B8=81?= =?UTF-8?q?=E0=B8=9B=E0=B8=A5=E0=B8=B1=E0=B8=94=20#2222?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileEditController.ts | 19 +++++++++++++++++++ .../ProfileEditEmployeeController.ts | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/controllers/ProfileEditController.ts b/src/controllers/ProfileEditController.ts index a1e081f3..35f26786 100644 --- a/src/controllers/ProfileEditController.ts +++ b/src/controllers/ProfileEditController.ts @@ -24,6 +24,7 @@ import CallAPI from "../interfaces/call-api"; import permission from "../interfaces/permission"; import { OrgRevision } from "../entities/OrgRevision"; import { OrgRoot } from "../entities/OrgRoot"; +import { PosMaster } from "../entities/PosMaster"; @Route("api/v1/org/profile/edit") @Tags("ProfileEdit") @Security("bearerAuth") @@ -32,6 +33,7 @@ export class ProfileEditController extends Controller { private profileEditRepo = AppDataSource.getRepository(ProfileEdit); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private orgRootRepo = AppDataSource.getRepository(OrgRoot); + private posMasterRepo = AppDataSource.getRepository(PosMaster); @Get("user") public async detailProfileEditUser( @@ -272,6 +274,22 @@ export class ProfileEditController extends Controller { if (!getProfileEdit) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); } + let orgRoot: OrgRoot | null = null; + if(getProfileEdit.profile) { + const empPosMaster = await this.posMasterRepo.findOne({ + where: { + current_holderId: getProfileEdit.profile.id, + orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false } + }, + relations: { orgRevision: true } + }); + if(empPosMaster) { + orgRoot = await this.orgRootRepo.findOne({ + select: { isDeputy: true }, + where: { id: empPosMaster.orgRootId ?? "" } + }); + } + } const _data = { id: getProfileEdit.id, topic: getProfileEdit.topic, @@ -289,6 +307,7 @@ export class ProfileEditController extends Controller { (getProfileEdit?.profile?.firstName ?? "") + " " + (getProfileEdit?.profile?.lastName ?? ""), + isDeputy: orgRoot?.isDeputy ?? false }; return new HttpSuccess(_data); } diff --git a/src/controllers/ProfileEditEmployeeController.ts b/src/controllers/ProfileEditEmployeeController.ts index 87bba20a..2cdd9fde 100644 --- a/src/controllers/ProfileEditEmployeeController.ts +++ b/src/controllers/ProfileEditEmployeeController.ts @@ -28,6 +28,7 @@ import permission from "../interfaces/permission"; import { OrgRevision } from "../entities/OrgRevision"; import { OrgRoot } from "../entities/OrgRoot"; import CallAPI from "../interfaces/call-api"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; @Route("api/v1/org/profile-employee/edit") @Tags("ProfileEmployeeEdit") @Security("bearerAuth") @@ -36,6 +37,7 @@ export class ProfileEditEmployeeController extends Controller { private profileEditRepository = AppDataSource.getRepository(ProfileEdit); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private orgRootRepo = AppDataSource.getRepository(OrgRoot); + private empPosMasterRepo = AppDataSource.getRepository(EmployeePosMaster); @Get("user") public async detailProfileEditUserEmp( @@ -271,6 +273,22 @@ export class ProfileEditEmployeeController extends Controller { if (!getProfileEdit) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); } + let orgRoot: OrgRoot | null = null; + if(getProfileEdit.profileEmployee) { + const empPosMaster = await this.empPosMasterRepo.findOne({ + where: { + current_holderId: getProfileEdit.profileEmployee.id, + orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false } + }, + relations: { orgRevision: true } + }); + if(empPosMaster) { + orgRoot = await this.orgRootRepo.findOne({ + select: { isDeputy: true }, + where: { id: empPosMaster.orgRootId ?? "" } + }); + } + } const _data = { id: getProfileEdit.id, topic: getProfileEdit.topic, @@ -288,6 +306,7 @@ export class ProfileEditEmployeeController extends Controller { (getProfileEdit?.profileEmployee?.firstName ?? "") + " " + (getProfileEdit?.profileEmployee?.lastName ?? ""), + isDeputy: orgRoot?.isDeputy ?? false }; return new HttpSuccess(_data); } From 81288f8db30fe0770b891aad4adb088fa1bc0b3b Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 6 Mar 2026 14:34:41 +0700 Subject: [PATCH 138/178] =?UTF-8?q?list=20=E0=B8=9B=E0=B8=A5=E0=B8=B1?= =?UTF-8?q?=E0=B8=94=20=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=A3=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B8=9B=E0=B8=A5=E0=B8=B1=E0=B8=94=E0=B9=83=E0=B8=99?= =?UTF-8?q?=20popup=20=E0=B8=9C=E0=B8=B9=E0=B9=89=E0=B8=9A=E0=B8=B1?= =?UTF-8?q?=E0=B8=87=E0=B8=84=E0=B8=B1=E0=B8=9A=E0=B8=9A=E0=B8=B1=E0=B8=8D?= =?UTF-8?q?=E0=B8=8A=E0=B8=B2=20=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=9C?= =?UTF-8?q?=E0=B8=B9=E0=B9=89=E0=B8=A1=E0=B8=B5=E0=B8=AD=E0=B8=B3=E0=B8=99?= =?UTF-8?q?=E0=B8=B2=E0=B8=88=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileController.ts | 26 ++++++++++++++++++++++ src/controllers/WorkflowController.ts | 32 +++++++++++++++++++-------- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 5fbfa83b..a9436648 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -2998,12 +2998,35 @@ export class ProfileController extends Controller { // console.log(condition); // console.log("------------------"); // console.log(conditionNow); + + // Task #2342 list ปลัด และรองปลัดใน popup ผู้บังคับบัญชา และผู้มีอำนาจเพิ่ม + let conditionisDeputy: any = { + isDeputy: true, + isDirector: true, + orgChild1Id: IsNull(), + orgChild2Id: IsNull(), + orgChild3Id: IsNull(), + orgChild4Id: IsNull(), + id: Not(posMaster.current_holderId), + }; + const orgRoot = await this.orgRootRepo.findOne({ + select: { id: true, isDeputy: true }, + where: { + id: Not(posMaster.orgRootId ?? ""), + isDeputy: true, + orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }, + }); + if (body.isAct == true) { const [lists, total] = await AppDataSource.getRepository(viewDirectorActing) .createQueryBuilder("viewDirectorActing") .andWhere( new Brackets((qb) => { qb.orWhere(condition).orWhere(conditionNow); + if (orgRoot && orgRoot.isDeputy) { + qb.orWhere(conditionisDeputy); + } }), ) .andWhere("viewDirectorActing.isProbation = :isProbation", { isProbation: false }) @@ -3069,6 +3092,9 @@ export class ProfileController extends Controller { .andWhere( new Brackets((qb) => { qb.orWhere(condition).orWhere(conditionNow); + if (orgRoot && orgRoot.isDeputy) { + qb.orWhere(conditionisDeputy); + } }), ) .andWhere("viewDirector.isProbation = :isProbation", { isProbation: false }) diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index d2438547..8e9d2cd4 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -22,7 +22,7 @@ import { viewDirectorActing } from "../entities/view/viewDirectorActing"; import { viewDirector } from "../entities/view/viewDirector"; import { ProfileEmployee } from "../entities/ProfileEmployee"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; - +import { OrgRoot } from "../entities/OrgRoot"; @Route("api/v1/org/workflow") @Tags("Workflow") @Security("bearerAuth") @@ -34,7 +34,7 @@ export class WorkflowController extends Controller { private stateUserCommentRepo = AppDataSource.getRepository(StateUserComment); private profileRepo = AppDataSource.getRepository(Profile); private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); - + private orgRootRepo = AppDataSource.getRepository(OrgRoot); private metaWorkflowRepo = AppDataSource.getRepository(MetaWorkflow); private metaStateRepo = AppDataSource.getRepository(MetaState); private metaStateOperatorRepo = AppDataSource.getRepository(MetaStateOperator); @@ -898,6 +898,20 @@ export class WorkflowController extends Controller { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบตำแหน่งผู้ใช้งาน"); } + // Task #2342 list ปลัด และรองปลัดใน popup ผู้บังคับบัญชา และผู้มีอำนาจเพิ่ม + const roodIds = [posMasterUser.orgRootId]; + const orgRoot = await this.orgRootRepo.findOne({ + select: { id: true, isDeputy: true }, + where: { + id: Not(posMasterUser.orgRootId), + isDeputy: true, + orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }, + }); + if (orgRoot && orgRoot.isDeputy) { + roodIds.push(orgRoot.id) + } + // 2. Pre-calculate conditions - ย้ายออกมาข้างนอก const posType = posMasterUser.current_holder?.posType?.posTypeName; const posLevel = posMasterUser.current_holder?.posLevel?.posLevelName; @@ -927,23 +941,23 @@ export class WorkflowController extends Controller { if (type.trim().toUpperCase() === "OPERATE" || body.type === "employee") { mainConditions = [ - { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: IsNull() }, + { ...baseCondition, orgRootId: In(roodIds), orgChild1Id: IsNull() }, { ...baseCondition, - orgRootId: posMasterUser.orgRootId, + orgRootId: In(roodIds), orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: IsNull(), }, { ...baseCondition, - orgRootId: posMasterUser.orgRootId, + orgRootId: In(roodIds), orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: IsNull(), }, { ...baseCondition, - orgRootId: posMasterUser.orgRootId, + orgRootId: In(roodIds), orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: posMasterUser.orgChild3Id, @@ -951,7 +965,7 @@ export class WorkflowController extends Controller { }, { ...baseCondition, - orgRootId: posMasterUser.orgRootId, + orgRootId: In(roodIds), orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: posMasterUser.orgChild3Id, @@ -962,7 +976,7 @@ export class WorkflowController extends Controller { mainConditions = [ { ...baseCondition, - orgRootId: posMasterUser.orgRootId, + orgRootId: In(roodIds), orgChild1Id: IsNull(), orgChild2Id: IsNull(), orgChild3Id: IsNull(), @@ -981,7 +995,7 @@ export class WorkflowController extends Controller { }, ]; } else { - mainConditions = [{ ...baseCondition, orgRootId: posMasterUser.orgRootId }]; + mainConditions = [{ ...baseCondition, orgRootId: In(roodIds) }]; } // 4. สร้าง optimized query builder From 76fc488d25c2b423f6e848d3a15d527d632c1341 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 6 Mar 2026 19:11:20 +0700 Subject: [PATCH 139/178] fix sync to keycloak --- src/controllers/KeycloakSyncController.ts | 26 +++- src/keycloak/index.ts | 116 +++++++++++++++ src/services/KeycloakAttributeService.ts | 171 +++++++++++++++++++++- 3 files changed, 303 insertions(+), 10 deletions(-) diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts index f24eee35..995fa3c0 100644 --- a/src/controllers/KeycloakSyncController.ts +++ b/src/controllers/KeycloakSyncController.ts @@ -236,10 +236,31 @@ export class KeycloakSyncController extends Controller { * * @description Syncs profileId and orgRootDnaId to Keycloak for all users * that have a keycloak ID. Uses parallel processing for better performance. + * + * Features: + * - Resume from checkpoint after failures (use resume=true) + * - Automatic retry with exponential backoff + * - Rate limiting to avoid overwhelming Keycloak + * - Progress tracking and persistence + * + * @param resume - Resume from last checkpoint (default: false) + * @param maxRetries - Maximum retry attempts for failed operations (default: 3) + * @param rateLimit - Requests per second rate limit (default: 10) + * @param clearProgress - Clear existing progress and start fresh (default: false) */ @Post("sync-all") - async syncAll() { - const result = await this.keycloakAttributeService.batchSyncUsers(); + async syncAll( + @Query() resume: boolean = false, + @Query() maxRetries: number = 3, + @Query() rateLimit: number = 10, + @Query() clearProgress: boolean = false, + ) { + const result = await this.keycloakAttributeService.batchSyncUsers({ + resume, + maxRetries, + rateLimit, + clearProgress, + }); return new HttpSuccess({ message: "Batch sync เสร็จสิ้น", @@ -247,6 +268,7 @@ export class KeycloakSyncController extends Controller { success: result.success, failed: result.failed, details: result.details, + resumed: result.resumed, }); } diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index 90ab019c..b661450c 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -1,5 +1,121 @@ import { DecodedJwt, createDecoder } from "fast-jwt"; +/** + * RateLimiter + * Limits the rate of API calls to avoid overwhelming the server + */ +export class RateLimiter { + private requestsPerSecond: number; + private requestTimes: number[] = []; + + constructor(requestsPerSecond: number = 10) { + this.requestsPerSecond = requestsPerSecond; + } + + /** + * Throttle requests to stay within rate limit + * Waits if rate limit would be exceeded + */ + async throttle(): Promise { + const now = Date.now(); + // Remove timestamps older than 1 second + this.requestTimes = this.requestTimes.filter((t) => now - t < 1000); + + if (this.requestTimes.length >= this.requestsPerSecond) { + const oldestRequest = this.requestTimes[0]; + const waitTime = 1000 - (now - oldestRequest); + if (waitTime > 0) { + await new Promise((resolve) => setTimeout(resolve, waitTime)); + } + } + + this.requestTimes.push(Date.now()); + } + + /** + * Reset the rate limiter (e.g., after a long pause) + */ + reset(): void { + this.requestTimes = []; + } +} + +/** + * Check if an error is a network error (retryable) + * @param error - Error to check + * @returns true if error is network-related and retryable + */ +function isNetworkError(error: any): boolean { + if (!error) return false; + + // Check for fetch network errors + if (error.name === "TypeError" && error.message.includes("fetch")) { + return true; + } + + // Check for ECONNREFUSED, ETIMEDOUT, etc. + if (error.code && ["ECONNREFUSED", "ETIMEDOUT", "ECONNRESET", "ENOTFOUND"].includes(error.code)) { + return true; + } + + return false; +} + +/** + * Check if an HTTP status code is retryable + * @param status - HTTP status code + * @returns true if status code indicates a temporary error + */ +function isRetryableStatus(status: number): boolean { + // Retry on 5xx errors (server errors) and 429 (rate limit) + return status >= 500 || status === 429; +} + +/** + * Retry wrapper with exponential backoff + * Retries failed operations with increasing delay between attempts + * + * @param fn - Function to execute + * @param maxRetries - Maximum number of retry attempts + * @param baseDelay - Base delay in milliseconds (doubles each retry) + * @returns Promise with result of fn + */ +export async function withRetry( + fn: () => Promise, + maxRetries: number = 3, + baseDelay: number = 1000, +): Promise { + let lastError: any; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error: any) { + lastError = error; + + // Check if error is retryable + const isRetryable = isNetworkError(error) || isRetryableStatus(error?.status); + + if (!isRetryable) { + // Don't retry on permanent errors (4xx except 429) + throw error; + } + + if (attempt < maxRetries) { + // Calculate delay with exponential backoff + const delay = baseDelay * Math.pow(2, attempt); + console.log( + `[withRetry] Attempt ${attempt + 1}/${maxRetries + 1} failed, retrying in ${delay}ms...`, + error.message || error, + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + throw lastError; +} + 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; diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index 53cce4b4..2b3af9ab 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -13,8 +13,11 @@ import { getRoles, addUserRoles, getAllUsersPaginated, + withRetry, + RateLimiter, } from "../keycloak"; import { OrgRevision } from "../entities/OrgRevision"; +import { SyncProgressManager, SyncProgressState } from "../utils/sync-progress"; export interface UserProfileAttributes { profileId: string | null; @@ -532,24 +535,62 @@ export class KeycloakAttributeService { * Batch sync multiple users with unlimited count and parallel processing * Useful for initial sync or periodic updates * - * @param options - Optional configuration (limit for testing, concurrency for parallel processing) + * Features: + * - Resume from checkpoint after failures + * - Automatic retry with exponential backoff + * - Rate limiting to avoid overwhelming Keycloak + * - Progress tracking and persistence + * + * @param options - Optional configuration * @returns Object with success count and details */ async batchSyncUsers(options?: { limit?: number; concurrency?: number; - }): Promise<{ total: number; success: number; failed: number; details: any[] }> { + resume?: boolean; // Resume from last checkpoint + maxRetries?: number; // Retry attempts for failed operations + rateLimit?: number; // Requests per second + clearProgress?: boolean; // Start fresh, ignore existing progress + }): Promise<{ total: number; success: number; failed: number; details: any[]; resumed?: boolean }> { const limit = options?.limit; const concurrency = options?.concurrency ?? 5; + const resume = options?.resume ?? false; + const maxRetries = options?.maxRetries ?? 3; + const rateLimit = options?.rateLimit ?? 10; + const clearProgress = options?.clearProgress ?? false; const result = { total: 0, success: 0, failed: 0, details: [] as any[], + resumed: false, }; + let progressState: SyncProgressState | null = null; + let rateLimiter: RateLimiter | null = null; + try { + // Handle progress file based on options + if (clearProgress) { + SyncProgressManager.clear(); + console.log("[batchSyncUsers] Cleared existing progress, starting fresh"); + } + + // Load existing progress if resume is requested + if (resume && !clearProgress) { + progressState = SyncProgressManager.load(); + if (progressState) { + result.resumed = true; + console.log( + `[batchSyncUsers] Resuming from checkpoint: ${progressState.lastSyncedIndex}/${progressState.totalProfiles}`, + ); + SyncProgressManager.logProgress(progressState); + } else { + console.log("[batchSyncUsers] No existing progress found, starting fresh"); + } + } + // Build query for profiles with keycloak IDs (ข้าราชการ) const profileQuery = this.profileRepo .createQueryBuilder("p") @@ -581,15 +622,44 @@ export class KeycloakAttributeService { result.total = allProfiles.length; + // Initialize or resume progress state + if (!progressState) { + const profileIds = allProfiles.map((p) => p.profile.id); + progressState = SyncProgressManager.initialize(profileIds); + SyncProgressManager.save(progressState); + console.log(`[batchSyncUsers] Starting sync of ${profileIds.length} profiles`); + } + + // Initialize rate limiter if rate limiting is enabled + if (rateLimit && rateLimit > 0) { + rateLimiter = new RateLimiter(rateLimit); + console.log(`[batchSyncUsers] Rate limiting enabled: ${rateLimit} requests/second`); + } + + // Determine starting index based on progress + let startIndex = progressState.lastSyncedIndex; + // Process in parallel with concurrency limit - const processedResults = await this.processInParallel( + const processedResults = await this.processInParallelWithProgress( allProfiles, concurrency, - async ({ profile, type }, _index) => { + startIndex, + async ({ profile, type }, index) => { + // Apply rate limiting if enabled + if (rateLimiter) { + await rateLimiter.throttle(); + } + const keycloakUserId = profile.keycloak; try { - const success = await this.syncOnOrganizationChange(profile.id, type); + // Wrap sync operation with retry logic + const success = await withRetry( + async () => this.syncOnOrganizationChange(profile.id, type), + maxRetries, + 1000, // Base delay: 1 second + ); + if (success) { result.success++; return { @@ -599,6 +669,13 @@ export class KeycloakAttributeService { }; } else { result.failed++; + // Add to failed profiles in progress state + SyncProgressManager.addFailedProfile( + progressState!, + index, + profile.id, + "Sync returned false", + ); return { profileId: profile.id, keycloakUserId, @@ -608,14 +685,30 @@ export class KeycloakAttributeService { } } catch (error: any) { result.failed++; + // Add to failed profiles in progress state + SyncProgressManager.addFailedProfile( + progressState!, + index, + profile.id, + error.message || String(error), + ); return { profileId: profile.id, keycloakUserId, status: "error", - error: error.message, + error: error.message || String(error), }; } }, + progressState, + (updatedState) => { + // Save progress after each batch + SyncProgressManager.save(updatedState); + // Log progress every 50 items + if (updatedState.lastSyncedIndex % 50 === 0 || updatedState.lastSyncedIndex === updatedState.totalProfiles) { + SyncProgressManager.logProgress(updatedState); + } + }, ); // Separate results from errors @@ -633,16 +726,78 @@ export class KeycloakAttributeService { } } + // Clear progress on successful completion + SyncProgressManager.clear(); + + const elapsed = SyncProgressManager.formatElapsedTime(progressState.startTime); console.log( - `Batch sync completed: total=${result.total}, success=${result.success}, failed=${result.failed}`, + `[batchSyncUsers] Completed: total=${result.total}, success=${result.success}, failed=${result.failed}, elapsed=${elapsed}`, ); + + // Log failed profiles summary + if (progressState.failedProfiles.length > 0) { + console.log( + `[batchSyncUsers] Failed profiles (${progressState.failedProfiles.length}):`, + progressState.failedProfiles.map((f) => `${f.profileId}(${f.error})`).join(", "), + ); + } } catch (error) { - console.error("Error in batch sync:", error); + console.error("[batchSyncUsers] Error in batch sync:", error); + // Save progress before throwing + if (progressState) { + SyncProgressManager.save(progressState); + console.log("[batchSyncUsers] Progress saved. Use resume=true to continue."); + } + throw error; } return result; } + /** + * Process items in parallel with concurrency limit and progress tracking + * Extends processInParallel with progress state management + */ + private async processInParallelWithProgress( + items: T[], + concurrencyLimit: number, + startIndex: number, + processor: (item: T, index: number) => Promise, + progressState: SyncProgressState, + onProgress?: (state: SyncProgressState) => void, + ): Promise> { + const results: Array = []; + + // Start from the saved checkpoint index + for (let i = startIndex; i < items.length; i += concurrencyLimit) { + const batch = items.slice(i, i + concurrencyLimit); + + // Process batch in parallel with error handling + const batchResults = await Promise.all( + batch.map(async (item, batchIndex) => { + const actualIndex = i + batchIndex; + try { + return await processor(item, actualIndex); + } catch (error) { + return { error }; + } + }), + ); + + results.push(...batchResults); + + // Update progress state + progressState.lastSyncedIndex = Math.min(i + concurrencyLimit, items.length); + + // Call progress callback + if (onProgress) { + onProgress(progressState); + } + } + + return results; + } + /** * Get current Keycloak attributes for a user * From 91887ec63d263f733e44c3ada53a2894455f9bcd Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Sat, 7 Mar 2026 21:14:50 +0700 Subject: [PATCH 140/178] fix ignore file --- src/utils/sync-progress.ts | 162 +++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/utils/sync-progress.ts diff --git a/src/utils/sync-progress.ts b/src/utils/sync-progress.ts new file mode 100644 index 00000000..afee9b20 --- /dev/null +++ b/src/utils/sync-progress.ts @@ -0,0 +1,162 @@ +import * as fs from "fs"; +import * as path from "path"; + +/** + * Interface for sync progress state + * Tracks the progress of batch sync operations + */ +export interface SyncProgressState { + lastSyncedIndex: number; + totalProfiles: number; + startTime: number; + lastUpdate: number; + failedProfiles: Array<{ index: number; profileId: string; error: string }>; + profileIds: string[]; // Track order of profile IDs for resume +} + +/** + * SyncProgressManager + * Manages persistent storage of sync progress state + * Enables resuming from checkpoints after failures + */ +export class SyncProgressManager { + private static readonly FILE_PATH = ".sync-progress.json"; + + /** + * Save progress state to file + * @param state - Current progress state to save + */ + static save(state: SyncProgressState): void { + try { + const filePath = path.resolve(process.cwd(), this.FILE_PATH); + state.lastUpdate = Date.now(); + fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8"); + } catch (error) { + console.error("[SyncProgressManager] Error saving progress:", error); + } + } + + /** + * Load progress state from file + * @returns Progress state if exists, null otherwise + */ + static load(): SyncProgressState | null { + try { + const filePath = path.resolve(process.cwd(), this.FILE_PATH); + if (!fs.existsSync(filePath)) { + return null; + } + + const data = fs.readFileSync(filePath, "utf-8"); + const state = JSON.parse(data) as SyncProgressState; + + // Validate required fields + if ( + typeof state.lastSyncedIndex !== "number" || + typeof state.totalProfiles !== "number" || + !Array.isArray(state.failedProfiles) || + !Array.isArray(state.profileIds) + ) { + console.warn("[SyncProgressManager] Invalid progress file, starting fresh"); + return null; + } + + return state; + } catch (error) { + console.error("[SyncProgressManager] Error loading progress:", error); + return null; + } + } + + /** + * Clear progress file + * Called on successful completion or when starting fresh + */ + static clear(): void { + try { + const filePath = path.resolve(process.cwd(), this.FILE_PATH); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + console.log("[SyncProgressManager] Progress file cleared"); + } + } catch (error) { + console.error("[SyncProgressManager] Error clearing progress:", error); + } + } + + /** + * Get progress percentage + * @param state - Current progress state + * @returns Percentage complete (0-100) + */ + static getProgressPercent(state: SyncProgressState): number { + if (state.totalProfiles === 0) return 0; + return Math.round((state.lastSyncedIndex / state.totalProfiles) * 100); + } + + /** + * Format elapsed time as readable string + * @param startTime - Start timestamp in milliseconds + * @returns Formatted time string (e.g., "2h 30m 15s") + */ + static formatElapsedTime(startTime: number): string { + const elapsed = Date.now() - startTime; + const seconds = Math.floor(elapsed / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + const parts: string[] = []; + if (hours > 0) parts.push(`${hours}h`); + if (minutes % 60 > 0) parts.push(`${minutes % 60}m`); + if (seconds % 60 > 0 || parts.length === 0) parts.push(`${seconds % 60}s`); + + return parts.join(" "); + } + + /** + * Log current progress to console + * @param state - Current progress state + */ + static logProgress(state: SyncProgressState): void { + const percent = this.getProgressPercent(state); + const elapsed = this.formatElapsedTime(state.startTime); + const failed = state.failedProfiles.length; + + console.log( + `[Sync Progress] ${percent}% (${state.lastSyncedIndex}/${state.totalProfiles}) | Elapsed: ${elapsed} | Failed: ${failed}`, + ); + } + + /** + * Add failed profile to state + * @param state - Progress state to update + * @param index - Profile index + * @param profileId - Profile ID that failed + * @param error - Error message + */ + static addFailedProfile( + state: SyncProgressState, + index: number, + profileId: string, + error: string, + ): void { + state.failedProfiles.push({ index, profileId, error }); + this.save(state); + } + + /** + * Initialize new progress state + * @param profileIds - Array of profile IDs to sync + * @returns New progress state + */ + static initialize(profileIds: string[]): SyncProgressState { + return { + lastSyncedIndex: 0, + totalProfiles: profileIds.length, + startTime: Date.now(), + lastUpdate: Date.now(), + failedProfiles: [], + profileIds, + }; + } +} From 060ac81532cc34b6cf1c606d18db0a35809f6944 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 9 Mar 2026 14:44:38 +0700 Subject: [PATCH 141/178] #2345 --- src/controllers/CommandController.ts | 298 ++++++++++-------- src/controllers/EmployeePositionController.ts | 8 + .../EmployeeTempPositionController.ts | 9 + src/controllers/PositionController.ts | 9 + 4 files changed, 201 insertions(+), 123 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index a62ddadc..24a0d17d 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -101,6 +101,7 @@ import { } from "../services/PositionService"; import { PostRetireToExprofile } from "./ExRetirementController"; import { LeaveType } from "../entities/LeaveType"; +import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; @Route("api/v1/org/command") @Tags("Command") @Security("bearerAuth") @@ -157,6 +158,8 @@ export class CommandController extends Controller { private genderRepo = AppDataSource.getRepository(Gender); private avatarRepository = AppDataSource.getRepository(ProfileAvatar); private leaveType = AppDataSource.getRepository(LeaveType); + private keycloakAttributeService = new KeycloakAttributeService(); + /** * API list รายการคำสั่ง * @@ -188,12 +191,12 @@ export class CommandController extends Controller { x.orgRevision?.orgRevisionIsCurrent == true && x.orgRevision?.orgRevisionIsDraft == false, )[0]?.isDirector || false; let _data: any = { - root: null, - child1: null, - child2: null, - child3: null, - child4: null, - }; + root: null, + child1: null, + child2: null, + child3: null, + child4: null, + }; if (!request.user.role.includes("SUPER_ADMIN")) { _data = await new permission().PermissionOrgList(request, "COMMAND"); } @@ -231,7 +234,7 @@ export class CommandController extends Controller { ? _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 null` : "1=1", { child1: _data.child1, @@ -309,7 +312,7 @@ export class CommandController extends Controller { status == null || status == undefined || status == "" ? null : status.trim().toLocaleUpperCase() == "NEW" || - status.trim().toLocaleUpperCase() == "DRAFT" + status.trim().toLocaleUpperCase() == "DRAFT" ? ["NEW", "DRAFT"] : [status.trim().toLocaleUpperCase()], }, @@ -810,8 +813,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -854,8 +857,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -898,8 +901,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -1183,8 +1186,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); await this.commandReciveRepository.delete({ commandId: command.id }); command.status = "CANCEL"; @@ -1249,8 +1252,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); await this.commandSendCCRepository.delete({ commandSendId: In(commandSend.map((x) => x.id)) }); await this.commandReciveRepository.delete({ commandId: command.id }); @@ -1403,11 +1406,11 @@ export class CommandController extends Controller { let profiles = command && command.commandRecives.length > 0 ? command.commandRecives - .filter((x) => x.profileId != null) - .map((x) => ({ - receiverUserId: x.profileId, - notiLink: "", - })) + .filter((x) => x.profileId != null) + .map((x) => ({ + receiverUserId: x.profileId, + notiLink: "", + })) : []; const msgNoti = { @@ -1439,8 +1442,8 @@ export class CommandController extends Controller { refIds: command.commandRecives.filter((x) => x.refId != null).map((x) => x.refId), status: "WAITING", }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); await this.commandRepository.save(command); } else { const path = commandTypePath(command.commandType.code); @@ -1577,7 +1580,7 @@ export class CommandController extends Controller { ); await this.profileRepository.save(profiles); } - } catch {} + } catch { } type = "EMPLOYEE"; try { @@ -1609,7 +1612,7 @@ export class CommandController extends Controller { ); await this.profileEmployeeRepository.save(profiles); } - } catch {} + } catch { } return new HttpSuccess(); } @@ -1673,7 +1676,7 @@ export class CommandController extends Controller { }), ); } - } catch {} + } catch { } type = "EMPLOYEE"; try { @@ -1728,7 +1731,7 @@ export class CommandController extends Controller { }), ); } - } catch {} + } catch { } return new HttpSuccess(); } @@ -1941,7 +1944,7 @@ export class CommandController extends Controller { .then((x) => { res = x; }) - .catch((x) => {}); + .catch((x) => { }); } let _command; @@ -2019,76 +2022,76 @@ export class CommandController extends Controller { profile?.current_holders.length == 0 ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild4 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4.orgChild4ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild3 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3.orgChild3ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2 != null + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild2 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2.orgChild2ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgChild1 != null + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + )?.orgChild1 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1.orgChild1ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgRoot != null + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + )?.orgRoot != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot.orgRootShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : null; const root = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgRoot; + ?.orgRoot; const child1 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild1; + ?.orgChild1; const child2 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild2; + ?.orgChild2; const child3 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild3; + ?.orgChild3; const child4 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild4; + ?.orgChild4; let _root = root?.orgRootName; let _child1 = child1?.orgChild1Name; @@ -2149,10 +2152,10 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.isLeave == false ? (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root) + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root) : orgLeave : profileTemp.org, fullName: `${x.prefix}${x.firstName} ${x.lastName}`, @@ -2167,8 +2170,8 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.posType && profile?.posLevel ? Extension.ToThaiNumber( - `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, - ) + `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, + ) : "-" : Extension.ToThaiNumber(profileTemp.posLevel), posNo: @@ -2182,19 +2185,19 @@ export class CommandController extends Controller { ? Extension.ToThaiNumber(Extension.ToThaiShortDate_monthYear(profile?.dateRetire)) : profile?.birthDate && commandCode == "C-PM-21" ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear( - new Date( - profile.birthDate.getFullYear() + 60, - profile.birthDate.getMonth(), - profile.birthDate.getDate(), - ), + Extension.ToThaiShortDate_monthYear( + new Date( + profile.birthDate.getFullYear() + 60, + profile.birthDate.getMonth(), + profile.birthDate.getDate(), ), - ) + ), + ) : "-", dateExecute: command.commandExcecuteDate ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), - ) + Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), + ) : "-", remark: x.remarkVertical ? x.remarkVertical : "-", }; @@ -2295,7 +2298,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => {}); + .catch(() => { }); let issue = command.isBangkok == "OFFICE" @@ -2353,15 +2356,15 @@ export class CommandController extends Controller { operators: operators.length > 0 ? operators.map((x) => ({ - fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, - roleName: x.roleName, - })) + fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, + roleName: x.roleName, + })) : [ - { - fullName: "", - roleName: "เจ้าหน้าที่ดำเนินการ", - }, - ], + { + fullName: "", + roleName: "เจ้าหน้าที่ดำเนินการ", + }, + ], }, }); } @@ -2666,8 +2669,8 @@ export class CommandController extends Controller { refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), status: "REPORT", }) - .then(async (res) => {}) - .catch(() => {}); + .then(async (res) => { }) + .catch(() => { }); let order = command.commandRecives == null || command.commandRecives.length <= 0 ? 0 @@ -3440,27 +3443,27 @@ export class CommandController extends Controller { ? x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName : x.orgChild3 == null ? x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName + : x.orgChild4 == null + ? x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + "\n" + x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName - : x.orgChild4 == null - ? x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName : x.orgChild4.orgChild4Name + - "\n" + - x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName, + "\n" + + x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName, positionName: x?.current_holder.position ?? _null, profileId: x?.current_holder.id ?? _null, }); @@ -4253,6 +4256,14 @@ export class CommandController extends Controller { profile.isActive = true; } 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], @@ -4499,6 +4510,14 @@ export class CommandController extends Controller { // 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 = ""; @@ -4714,6 +4733,13 @@ export class CommandController extends Controller { profile.amount = item.amount ?? _null; profile.amountSpecial = item.amountSpecial ?? _null; await this.profileRepository.save(profile, { data: req }); + + if (profile.id) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [profile.id], + "PROFILE", + ); + } } Object.assign(data, { ...item, ...meta }); const history = new ProfileSalaryHistory(); @@ -5198,6 +5224,12 @@ export class CommandController extends Controller { // _profile.posLevelId = _null; } await this.profileRepository.save(_profile); + if (_profile.id) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [_profile.id], + "PROFILE", + ); + } } } // ลูกจ้าง @@ -5375,6 +5407,12 @@ export class CommandController extends Controller { // _profile.posLevelId = _null; } await this.profileEmployeeRepository.save(_profile); + if (_profile.id) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [_profile.id], + "PROFILE_EMPLOYEE", + ); + } } } // Task #2190 @@ -5706,6 +5744,13 @@ export class CommandController extends Controller { // _profile.posLevelId = _null; } await this.profileEmployeeRepository.save(_profile); + + if (_profile.id) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [_profile.id], + "PROFILE_EMPLOYEE", + ); + } } }), ); @@ -6040,26 +6085,26 @@ export class CommandController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild4 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; @@ -6139,6 +6184,14 @@ export class CommandController extends Controller { this.profileRepository.save(_profile), this.salaryRepo.save(profileSalary), ]); + + if (profile.id) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [profile.id], + "PROFILE", + ); + } + const history = new ProfileSalaryHistory(); Object.assign(history, { ...profileSalary, id: undefined }); history.profileSalaryId = profileSalary.id; @@ -6170,7 +6223,6 @@ export class CommandController extends Controller { ); }), ); - return new HttpSuccess(); } @@ -6883,8 +6935,8 @@ export class CommandController extends Controller { prefix: avatar, fileName: fileName, }) - .then(() => {}) - .catch(() => {}); + .then(() => { }) + .catch(() => { }); } } }), @@ -7995,7 +8047,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => {}); + .catch(() => { }); let issue = command.isBangkok == "OFFICE" diff --git a/src/controllers/EmployeePositionController.ts b/src/controllers/EmployeePositionController.ts index 39f239c4..7b09973e 100644 --- a/src/controllers/EmployeePositionController.ts +++ b/src/controllers/EmployeePositionController.ts @@ -43,6 +43,7 @@ import { CreatePosMasterHistoryOfficer, } from "../services/PositionService"; import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; +import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; @Route("api/v1/org/employee/pos") @Tags("Employee") @Security("bearerAuth") @@ -65,6 +66,7 @@ export class EmployeePositionController extends Controller { private child3Repository = AppDataSource.getRepository(OrgChild3); private child4Repository = AppDataSource.getRepository(OrgChild4); private authRoleRepo = AppDataSource.getRepository(AuthRole); + private keycloakAttributeService = new KeycloakAttributeService(); /** * API เพิ่มตำแหน่งลูกจ้างประจำ @@ -2435,6 +2437,12 @@ export class EmployeePositionController extends Controller { // await this.profileRepository.save(profile); // } // } + if (dataMaster.current_holderId) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [dataMaster.current_holderId], + "PROFILE_EMPLOYEE", + ); + } await this.employeePosMasterRepository.update(id, { isSit: false, diff --git a/src/controllers/EmployeeTempPositionController.ts b/src/controllers/EmployeeTempPositionController.ts index d7ffdf62..69dc3b92 100644 --- a/src/controllers/EmployeeTempPositionController.ts +++ b/src/controllers/EmployeeTempPositionController.ts @@ -43,6 +43,7 @@ import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; import { CreatePosMasterHistoryEmployeeTemp } from "../services/PositionService"; import { PosMasterEmployeeTempHistory } from "../entities/PosMasterEmployeeTempHistory"; +import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; @Route("api/v1/org/employee-temp/pos") @Tags("Employee") @Security("bearerAuth") @@ -65,6 +66,7 @@ export class EmployeeTempPositionController extends Controller { private child3Repository = AppDataSource.getRepository(OrgChild3); private child4Repository = AppDataSource.getRepository(OrgChild4); private authRoleRepo = AppDataSource.getRepository(AuthRole); + private keycloakAttributeService = new KeycloakAttributeService(); /** * API เพิ่มตำแหน่งลูกจ้างประจำ @@ -2141,6 +2143,13 @@ export class EmployeeTempPositionController extends Controller { // } // } + if (dataMaster.current_holderId) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [dataMaster.current_holderId], + "PROFILE_EMPLOYEE", + ); + } + await this.employeeTempPosMasterRepository.update(id, { isSit: false, next_holderId: null, diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index f35c3632..3fff25c4 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -44,6 +44,7 @@ import { Assign } from "../entities/Assign"; import { ProfileEmployee } from "../entities/ProfileEmployee"; import { PosMasterHistory } from "../entities/PosMasterHistory"; import { CreatePosMasterHistoryOfficer } from "../services/PositionService"; +import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; @Route("api/v1/org/pos") @Tags("Position") @Security("bearerAuth") @@ -73,6 +74,7 @@ export class PositionController extends Controller { private authRoleRepo = AppDataSource.getRepository(AuthRole); private posMasterAssignRepo = AppDataSource.getRepository(PosMasterAssign); private assignRepo = AppDataSource.getRepository(Assign); + private keycloakAttributeService = new KeycloakAttributeService(); /** * API เพิ่มตำแหน่ง @@ -3868,6 +3870,13 @@ export class PositionController extends Controller { await CreatePosMasterHistoryOfficer(dataMaster.id, request, "DELETE"); } + if (dataMaster.current_holderId) { + await this.keycloakAttributeService.clearOrgDnaAttributes( + [dataMaster.current_holderId], + "PROFILE", + ); + } + let _profileId: string = ""; if (dataMaster?.current_holderId) { _profileId = dataMaster?.current_holderId; From 51028052781ea486e8264e3e142d454b90dbdd92 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 9 Mar 2026 16:17:26 +0700 Subject: [PATCH 142/178] comment update dna(keycloak) when command --- src/controllers/CommandController.ts | 84 ++++++++++++++-------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 24a0d17d..fa0e7172 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -4257,12 +4257,12 @@ export class CommandController extends Controller { } await this.profileRepository.save(profile); - if (profile.id) { - await this.keycloakAttributeService.clearOrgDnaAttributes( - [profile.id], - "PROFILE", - ); - } + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE", + // ); + // } // update user attribute in keycloak await updateUserAttributes(profile.keycloak ?? "", { @@ -4511,12 +4511,12 @@ export class CommandController extends Controller { } await this.profileEmployeeRepository.save(profile); - if (profile.id) { - await this.keycloakAttributeService.clearOrgDnaAttributes( - [profile.id], - "PROFILE_EMPLOYEE", - ); - } + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE_EMPLOYEE", + // ); + // } // Task #2190 if (code && ["C-PM-23", "C-PM-43"].includes(code)) { @@ -4734,12 +4734,12 @@ export class CommandController extends Controller { profile.amountSpecial = item.amountSpecial ?? _null; await this.profileRepository.save(profile, { data: req }); - if (profile.id) { - await this.keycloakAttributeService.clearOrgDnaAttributes( - [profile.id], - "PROFILE", - ); - } + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE", + // ); + // } } Object.assign(data, { ...item, ...meta }); const history = new ProfileSalaryHistory(); @@ -5224,12 +5224,12 @@ export class CommandController extends Controller { // _profile.posLevelId = _null; } await this.profileRepository.save(_profile); - if (_profile.id) { - await this.keycloakAttributeService.clearOrgDnaAttributes( - [_profile.id], - "PROFILE", - ); - } + // if (_profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [_profile.id], + // "PROFILE", + // ); + // } } } // ลูกจ้าง @@ -5407,12 +5407,12 @@ export class CommandController extends Controller { // _profile.posLevelId = _null; } await this.profileEmployeeRepository.save(_profile); - if (_profile.id) { - await this.keycloakAttributeService.clearOrgDnaAttributes( - [_profile.id], - "PROFILE_EMPLOYEE", - ); - } + // if (_profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [_profile.id], + // "PROFILE_EMPLOYEE", + // ); + // } } } // Task #2190 @@ -5745,12 +5745,12 @@ export class CommandController extends Controller { } await this.profileEmployeeRepository.save(_profile); - if (_profile.id) { - await this.keycloakAttributeService.clearOrgDnaAttributes( - [_profile.id], - "PROFILE_EMPLOYEE", - ); - } + // if (_profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [_profile.id], + // "PROFILE_EMPLOYEE", + // ); + // } } }), ); @@ -6185,12 +6185,12 @@ export class CommandController extends Controller { this.salaryRepo.save(profileSalary), ]); - if (profile.id) { - await this.keycloakAttributeService.clearOrgDnaAttributes( - [profile.id], - "PROFILE", - ); - } + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE", + // ); + // } const history = new ProfileSalaryHistory(); Object.assign(history, { ...profileSalary, id: undefined }); From baa8496a69fc2935e8c66c28d7aa5438b010d409 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Tue, 10 Mar 2026 11:45:25 +0700 Subject: [PATCH 143/178] refactor api act/{id} --- src/controllers/OrganizationController.ts | 575 ++++++++++------------ src/interfaces/OrgTypes.ts | 10 + src/utils/org-formatting.ts | 70 +++ 3 files changed, 336 insertions(+), 319 deletions(-) create mode 100644 src/interfaces/OrgTypes.ts create mode 100644 src/utils/org-formatting.ts diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index fdf24b66..e1767a43 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -65,6 +65,8 @@ import { } from "../services/PositionService"; 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"; @Route("api/v1/org") @Tags("Organization") @@ -5678,7 +5680,7 @@ export class OrganizationController extends Controller { */ @Get("act/{id}") async detailAct(@Path() id: string, @Request() request: RequestWithUser) { - let _data: any = { + let _data: OrgPermissionData = { root: null, child1: null, child2: null, @@ -5721,7 +5723,7 @@ export class OrganizationController extends Controller { if (profileAssign && _privilege.privilege !== "OWNER" && _privilege.privilege !== "PARENT") { if (_privilege.privilege == "NORMAL") { const holder = profile.current_holders.find((x) => x.orgRevisionId === id); - if (!holder) return; + if (!holder) return new HttpSuccess({ remark: "", data: [] }); _data.root = [holder.orgRootId]; _data.child1 = [holder.orgChild1Id]; _data.child2 = [holder.orgChild2Id]; @@ -5729,7 +5731,7 @@ export class OrganizationController extends Controller { _data.child4 = [holder.orgChild4Id]; } else if (_privilege.privilege == "CHILD" || _privilege.privilege == "BROTHER") { const holder = profile.current_holders.find((x) => x.orgRevisionId === id); - if (!holder) return; + if (!holder) return new HttpSuccess({ remark: "", data: [] }); _data.root = [holder.orgRootId]; if (_privilege.root && _privilege.child1 === null) { } else if (_privilege.child1 && _privilege.child2 === null) { @@ -5752,10 +5754,24 @@ export class OrganizationController extends Controller { } const orgDna = await new permission().checkDna(request, request.user.sub); - let level: any = resolveNodeLevel(orgDna); + let level: NodeLevel = resolveNodeLevel(orgDna); const orgRootData = await AppDataSource.getRepository(OrgRoot) .createQueryBuilder("orgRoot") + .select([ + "orgRoot.id", + "orgRoot.orgRootName", + "orgRoot.orgRootShortName", + "orgRoot.orgRootCode", + "orgRoot.orgRootOrder", + ]) + .addSelect([ + "posMasters.id", + "posMasters.posMasterNo", + "posMasters.orgChild1Id", + "posMasters.isDirector", + ]) + .addSelect(["current_holder.prefix", "current_holder.firstName", "current_holder.lastName"]) .where("orgRoot.orgRevisionId = :id", { id }) .andWhere( _data.root != undefined && _data.root != null @@ -5767,8 +5783,8 @@ export class OrganizationController extends Controller { node: _data.root, }, ) - .leftJoinAndSelect("orgRoot.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .leftJoin("orgRoot.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgRoot.orgRootOrder", "ASC") .getMany(); @@ -5777,6 +5793,25 @@ export class OrganizationController extends Controller { orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) .createQueryBuilder("orgChild1") + .select([ + "orgChild1.id", + "orgChild1.orgRootId", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + ]) + .addSelect([ + "posMasters.id", + "posMasters.posMasterNo", + "posMasters.orgChild2Id", + "posMasters.isDirector", + ]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) .andWhere( _data.child1 != undefined && _data.child1 != null @@ -5788,8 +5823,8 @@ export class OrganizationController extends Controller { node: _data.child1, }, ) - .leftJoinAndSelect("orgChild1.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .leftJoin("orgChild1.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild1.orgChild1Order", "ASC") .getMany() : []; @@ -5799,6 +5834,25 @@ export class OrganizationController extends Controller { orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) .createQueryBuilder("orgChild2") + .select([ + "orgChild2.id", + "orgChild2.orgChild1Id", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + ]) + .addSelect([ + "posMasters.id", + "posMasters.posMasterNo", + "posMasters.orgChild3Id", + "posMasters.isDirector", + ]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) .andWhere( _data.child2 != undefined && _data.child2 != null @@ -5810,8 +5864,8 @@ export class OrganizationController extends Controller { node: _data.child2, }, ) - .leftJoinAndSelect("orgChild2.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .leftJoin("orgChild2.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild2.orgChild2Order", "ASC") .getMany() : []; @@ -5821,6 +5875,25 @@ export class OrganizationController extends Controller { orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) .createQueryBuilder("orgChild3") + .select([ + "orgChild3.id", + "orgChild3.orgChild2Id", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + ]) + .addSelect([ + "posMasters.id", + "posMasters.posMasterNo", + "posMasters.orgChild4Id", + "posMasters.isDirector", + ]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) .andWhere( _data.child3 != undefined && _data.child3 != null @@ -5832,8 +5905,8 @@ export class OrganizationController extends Controller { node: _data.child3, }, ) - .leftJoinAndSelect("orgChild3.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .leftJoin("orgChild3.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild3.orgChild3Order", "ASC") .getMany() : []; @@ -5843,6 +5916,20 @@ export class OrganizationController extends Controller { orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) .createQueryBuilder("orgChild4") + .select([ + "orgChild4.id", + "orgChild4.orgChild3Id", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + ]) + .addSelect(["posMasters.id", "posMasters.posMasterNo", "posMasters.isDirector"]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) .andWhere( _data.child4 != undefined && _data.child4 != null @@ -5854,333 +5941,183 @@ export class OrganizationController extends Controller { node: _data.child4, }, ) - .leftJoinAndSelect("orgChild4.posMasters", "posMasters") - .leftJoinAndSelect("posMasters.current_holder", "current_holder") + .leftJoin("orgChild4.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild4.orgChild4Order", "ASC") .getMany() : []; const cannotViewRootPosMaster = _privilege.privilege === "PARENT" || - (_privilege.privilege === "BROTHER" && level > 1) || - (_privilege.privilege === "CHILD" && level > 0) || - (_privilege.privilege === "NORMAL" && level != 0); + (_privilege.privilege === "BROTHER" && level !== null && level > 1) || + (_privilege.privilege === "CHILD" && level !== null && level > 0) || + (_privilege.privilege === "NORMAL" && level !== null && level !== 0); const cannotViewChild1PosMaster = - (_privilege.privilege === "PARENT" && level > 1) || - (_privilege.privilege === "BROTHER" && level > 2) || - (_privilege.privilege === "CHILD" && level > 1) || + (_privilege.privilege === "PARENT" && level !== null && level > 1) || + (_privilege.privilege === "BROTHER" && level !== null && level > 2) || + (_privilege.privilege === "CHILD" && level !== null && level > 1) || (_privilege.privilege === "NORMAL" && level !== 1); const cannotViewChild2PosMaster = - (_privilege.privilege === "PARENT" && level > 2) || - (_privilege.privilege === "BROTHER" && level > 3) || - (_privilege.privilege === "CHILD" && level > 2) || + (_privilege.privilege === "PARENT" && level !== null && level > 2) || + (_privilege.privilege === "BROTHER" && level !== null && level > 3) || + (_privilege.privilege === "CHILD" && level !== null && level > 2) || (_privilege.privilege === "NORMAL" && level !== 2); const cannotViewChild3PosMaster = - (_privilege.privilege === "PARENT" && level > 3) || - (_privilege.privilege === "BROTHER" && level > 4) || - (_privilege.privilege === "CHILD" && level > 3) || + (_privilege.privilege === "PARENT" && level !== null && level > 3) || + (_privilege.privilege === "BROTHER" && level !== null && level > 4) || + (_privilege.privilege === "CHILD" && level !== null && level > 3) || (_privilege.privilege === "NORMAL" && level !== 3); const cannotViewChild4PosMaster = - (_privilege.privilege === "PARENT" && level > 4) || - (_privilege.privilege === "CHILD" && level > 4) || + (_privilege.privilege === "PARENT" && level !== null && level > 4) || + (_privilege.privilege === "CHILD" && level !== null && level > 4) || (_privilege.privilege === "NORMAL" && level !== 4); - // const formattedData = orgRootData.map((orgRoot) => { - const formattedData = await Promise.all( - orgRootData.map(async (orgRoot) => { - return { - orgTreeId: orgRoot.id, - orgLevel: 0, - orgName: orgRoot.orgRootName, - orgTreeName: orgRoot.orgRootName, - orgTreeShortName: orgRoot.orgRootShortName, - orgTreeCode: orgRoot.orgRootCode, - orgCode: orgRoot.orgRootCode + "00", - orgRootName: orgRoot.orgRootName, - labelName: - orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, - posMaster: cannotViewRootPosMaster - ? [] - : await Promise.all( - orgRoot.posMasters - .filter( - (x) => - x.orgChild1Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgRoot.orgRootShortName} ${x.posMasterNo}`, - orgTreeId: orgRoot.id, - orgLevel: 0, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), - children: await Promise.all( - orgChild1Data - .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) - .map(async (orgChild1) => ({ - orgTreeId: orgChild1.id, - orgRootId: orgRoot.id, - orgLevel: 1, - orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild1.orgChild1Name, - orgTreeShortName: orgChild1.orgChild1ShortName, - orgTreeCode: orgChild1.orgChild1Code, - orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code, - orgRootName: orgRoot.orgRootName, - labelName: - orgChild1.orgChild1Name + - " " + - orgRoot.orgRootCode + - orgChild1.orgChild1Code + - " " + - orgChild1.orgChild1ShortName + - "/" + - orgRoot.orgRootName + - " " + - orgRoot.orgRootCode + - "00" + - " " + - orgRoot.orgRootShortName, - posMaster: cannotViewChild1PosMaster - ? [] - : await Promise.all( - orgChild1.posMasters - .filter( - (x) => - x.orgChild2Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild1.orgChild1ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild1.id, - orgLevel: 1, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), - - children: await Promise.all( - orgChild2Data - .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) - .map(async (orgChild2) => ({ - orgTreeId: orgChild2.id, - orgRootId: orgChild1.id, - orgLevel: 2, - orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild2.orgChild2Name, - orgTreeShortName: orgChild2.orgChild2ShortName, - orgTreeCode: orgChild2.orgChild2Code, - orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code, - orgRootName: orgRoot.orgRootName, - labelName: - orgChild2.orgChild2Name + - " " + - orgRoot.orgRootCode + - orgChild2.orgChild2Code + - " " + - orgChild2.orgChild2ShortName + - "/" + - orgChild1.orgChild1Name + - " " + - orgRoot.orgRootCode + - orgChild1.orgChild1Code + - " " + - orgChild1.orgChild1ShortName + - "/" + - orgRoot.orgRootName + - " " + - orgRoot.orgRootCode + - "00" + - " " + - orgRoot.orgRootShortName, - posMaster: cannotViewChild2PosMaster - ? [] - : await Promise.all( - orgChild2.posMasters - .filter( - (x) => - x.orgChild3Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild2.orgChild2ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild2.id, - orgLevel: 2, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), - - children: await Promise.all( - orgChild3Data - .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) - .map(async (orgChild3) => ({ - orgTreeId: orgChild3.id, - orgRootId: orgChild2.id, - orgLevel: 3, - orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild3.orgChild3Name, - orgTreeShortName: orgChild3.orgChild3ShortName, - orgTreeCode: orgChild3.orgChild3Code, - orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code, - orgRootName: orgRoot.orgRootName, - labelName: - orgChild3.orgChild3Name + - " " + - orgRoot.orgRootCode + - orgChild3.orgChild3Code + - " " + - orgChild3.orgChild3ShortName + - "/" + - orgChild2.orgChild2Name + - " " + - orgRoot.orgRootCode + - orgChild2.orgChild2Code + - " " + - orgChild2.orgChild2ShortName + - "/" + - orgChild1.orgChild1Name + - " " + - orgRoot.orgRootCode + - orgChild1.orgChild1Code + - " " + - orgChild1.orgChild1ShortName + - "/" + - orgRoot.orgRootName + - " " + - orgRoot.orgRootCode + - "00" + - " " + - orgRoot.orgRootShortName, - posMaster: cannotViewChild3PosMaster - ? [] - : await Promise.all( - orgChild3.posMasters - .filter( - (x) => - x.orgChild4Id == null && - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild3.orgChild3ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild3.id, - orgLevel: 3, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), - - children: await Promise.all( - orgChild4Data - .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) - .map(async (orgChild4) => ({ - orgTreeId: orgChild4.id, - orgRootId: orgChild3.id, - orgLevel: 4, - orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, - orgTreeName: orgChild4.orgChild4Name, - orgTreeShortName: orgChild4.orgChild4ShortName, - orgTreeCode: orgChild4.orgChild4Code, - orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code, - orgRootName: orgRoot.orgRootName, - labelName: - orgChild4.orgChild4Name + - " " + - orgRoot.orgRootCode + - orgChild4.orgChild4Code + - " " + - orgChild4.orgChild4ShortName + - "/" + - orgChild3.orgChild3Name + - " " + - orgRoot.orgRootCode + - orgChild3.orgChild3Code + - " " + - orgChild3.orgChild3ShortName + - "/" + - orgChild2.orgChild2Name + - " " + - orgRoot.orgRootCode + - orgChild2.orgChild2Code + - " " + - orgChild2.orgChild2ShortName + - "/" + - orgChild1.orgChild1Name + - " " + - orgRoot.orgRootCode + - orgChild1.orgChild1Code + - " " + - orgChild1.orgChild1ShortName + - "/" + - orgRoot.orgRootName + - " " + - orgRoot.orgRootCode + - "00" + - " " + - orgRoot.orgRootShortName, - posMaster: cannotViewChild4PosMaster - ? [] - : await Promise.all( - orgChild4.posMasters - .filter( - (x) => - // x.current_holderId != null && - x.isDirector === true, - ) - // .sort((a, b) => a.posMasterOrder - b.posMasterOrder) // Sort by posMasterOrder ASC - // .slice(0, 3) // Select the first 3 rows - .map(async (x) => ({ - posmasterId: x.id, - posNo: `${orgChild4.orgChild4ShortName} ${x.posMasterNo}`, - orgTreeId: orgChild4.id, - orgLevel: 4, - fullNameCurrentHolder: - x.current_holder == null - ? null - : `${x.current_holder.prefix}${x.current_holder.firstName} ${x.current_holder.lastName}`, - })), - ), - })), - ), - })), - ), - })), - ), - })), + const formattedData = orgRootData.map((orgRoot) => ({ + orgTreeId: orgRoot.id, + orgLevel: 0, + orgName: orgRoot.orgRootName, + orgTreeName: orgRoot.orgRootName, + orgTreeShortName: orgRoot.orgRootShortName, + orgTreeCode: orgRoot.orgRootCode, + orgCode: orgRoot.orgRootCode + "00", + orgRootName: orgRoot.orgRootName, + labelName: generateLabelName( + orgRoot.orgRootName, + orgRoot.orgRootCode + "00", + orgRoot.orgRootShortName, + orgRoot.orgRootName, + orgRoot.orgRootCode, + orgRoot.orgRootShortName, + ), + posMaster: cannotViewRootPosMaster + ? [] + : filterPosMasters(orgRoot.posMasters, "orgChild1Id").map((x) => + formatPosMaster(x, orgRoot.orgRootShortName, orgRoot.id, 0), ), - }; - }), - ); + children: orgChild1Data + .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) + .map((orgChild1) => ({ + orgTreeId: orgChild1.id, + orgRootId: orgRoot.id, + orgLevel: 1, + orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild1.orgChild1Name, + orgTreeShortName: orgChild1.orgChild1ShortName, + orgTreeCode: orgChild1.orgChild1Code, + orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code, + orgRootName: orgRoot.orgRootName, + labelName: generateLabelName( + orgChild1.orgChild1Name, + orgChild1.orgChild1Code, + orgChild1.orgChild1ShortName, + orgRoot.orgRootName, + orgRoot.orgRootCode, + orgRoot.orgRootShortName, + ), + posMaster: cannotViewChild1PosMaster + ? [] + : filterPosMasters(orgChild1.posMasters, "orgChild2Id").map((x) => + formatPosMaster(x, orgChild1.orgChild1ShortName, orgChild1.id, 1), + ), + children: orgChild2Data + .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) + .map((orgChild2) => ({ + orgTreeId: orgChild2.id, + orgRootId: orgChild1.id, + orgLevel: 2, + orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild2.orgChild2Name, + orgTreeShortName: orgChild2.orgChild2ShortName, + orgTreeCode: orgChild2.orgChild2Code, + orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code, + orgRootName: orgRoot.orgRootName, + labelName: generateLabelName( + orgChild2.orgChild2Name, + orgChild2.orgChild2Code, + orgChild2.orgChild2ShortName, + orgRoot.orgRootName, + orgRoot.orgRootCode, + orgRoot.orgRootShortName, + [orgChild1.orgChild1Name], + [orgChild1.orgChild1Code], + [orgChild1.orgChild1ShortName], + ), + posMaster: cannotViewChild2PosMaster + ? [] + : filterPosMasters(orgChild2.posMasters, "orgChild3Id").map((x) => + formatPosMaster(x, orgChild2.orgChild2ShortName, orgChild2.id, 2), + ), + children: orgChild3Data + .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) + .map((orgChild3) => ({ + orgTreeId: orgChild3.id, + orgRootId: orgChild2.id, + orgLevel: 3, + orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild3.orgChild3Name, + orgTreeShortName: orgChild3.orgChild3ShortName, + orgTreeCode: orgChild3.orgChild3Code, + orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code, + orgRootName: orgRoot.orgRootName, + labelName: generateLabelName( + orgChild3.orgChild3Name, + orgChild3.orgChild3Code, + orgChild3.orgChild3ShortName, + orgRoot.orgRootName, + orgRoot.orgRootCode, + orgRoot.orgRootShortName, + [orgChild2.orgChild2Name, orgChild1.orgChild1Name], + [orgChild2.orgChild2Code, orgChild1.orgChild1Code], + [orgChild2.orgChild2ShortName, orgChild1.orgChild1ShortName], + ), + posMaster: cannotViewChild3PosMaster + ? [] + : filterPosMasters(orgChild3.posMasters, "orgChild4Id").map((x) => + formatPosMaster(x, orgChild3.orgChild3ShortName, orgChild3.id, 3), + ), + children: orgChild4Data + .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) + .map((orgChild4) => ({ + orgTreeId: orgChild4.id, + orgRootId: orgChild3.id, + orgLevel: 4, + orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`, + orgTreeName: orgChild4.orgChild4Name, + orgTreeShortName: orgChild4.orgChild4ShortName, + orgTreeCode: orgChild4.orgChild4Code, + orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code, + orgRootName: orgRoot.orgRootName, + labelName: generateLabelName( + orgChild4.orgChild4Name, + orgChild4.orgChild4Code, + orgChild4.orgChild4ShortName, + orgRoot.orgRootName, + orgRoot.orgRootCode, + orgRoot.orgRootShortName, + [orgChild3.orgChild3Name, orgChild2.orgChild2Name, orgChild1.orgChild1Name], + [orgChild3.orgChild3Code, orgChild2.orgChild2Code, orgChild1.orgChild1Code], + [ + orgChild3.orgChild3ShortName, + orgChild2.orgChild2ShortName, + orgChild1.orgChild1ShortName, + ], + ), + posMaster: cannotViewChild4PosMaster + ? [] + : orgChild4.posMasters + .filter((x) => x.isDirector === true) + .map((x) => + formatPosMaster(x, orgChild4.orgChild4ShortName, orgChild4.id, 4), + ), + })), + })), + })), + })), + })); return new HttpSuccess(formattedData); } diff --git a/src/interfaces/OrgTypes.ts b/src/interfaces/OrgTypes.ts new file mode 100644 index 00000000..12402cad --- /dev/null +++ b/src/interfaces/OrgTypes.ts @@ -0,0 +1,10 @@ +export interface OrgPermissionData { + root: (string | null | undefined)[] | null; + child1: (string | null | undefined)[] | null; + child2: (string | null | undefined)[] | null; + child3: (string | null | undefined)[] | null; + child4: (string | null | undefined)[] | null; + privilege?: "OWNER" | "PARENT" | "CHILD" | "BROTHER" | "NORMAL"; +} + +export type NodeLevel = 0 | 1 | 2 | 3 | 4 | null; diff --git a/src/utils/org-formatting.ts b/src/utils/org-formatting.ts new file mode 100644 index 00000000..701fb478 --- /dev/null +++ b/src/utils/org-formatting.ts @@ -0,0 +1,70 @@ +import { PosMaster } from "../entities/PosMaster"; + +export interface PosMasterFormatted { + posmasterId: string; + posNo: string; + orgTreeId: string; + orgLevel: number; + fullNameCurrentHolder: string | null; +} + +export interface OrgTreeNode { + orgTreeId: string; + orgLevel: number; + orgName: string; + orgTreeName: string; + orgTreeShortName: string; + orgTreeCode: string; + orgCode: string; + orgRootName: string; + labelName: string; + posMaster: PosMasterFormatted[]; + children?: OrgTreeNode[]; +} + +export function formatPosMaster( + posMaster: PosMaster, + orgShortName: string, + orgTreeId: string, + orgLevel: number, +): PosMasterFormatted { + return { + posmasterId: posMaster.id, + posNo: `${orgShortName} ${posMaster.posMasterNo}`, + orgTreeId, + orgLevel, + fullNameCurrentHolder: posMaster.current_holder + ? `${posMaster.current_holder.prefix}${posMaster.current_holder.firstName} ${posMaster.current_holder.lastName}` + : null, + }; +} + +export function generateLabelName( + nodeName: string, + nodeCode: string, + nodeShortName: string, + rootName: string, + rootCode: string, + rootShortName: string, + parentNames?: string[], + parentCodes?: string[], + parentShortNames?: string[], +): string { + const parts = [nodeName, " ", rootCode, nodeCode, " ", nodeShortName]; + + if (parentNames) { + for (let i = 0; i < parentNames.length; i++) { + parts.push("/", parentNames[i], " ", rootCode, parentCodes![i], " ", parentShortNames![i]); + } + } + + parts.push("/", rootName, " ", rootCode, "00", " ", rootShortName); + return parts.join(""); +} + +export function filterPosMasters( + posMasters: PosMaster[], + childLevelIdKey: keyof PosMaster, +): PosMaster[] { + return posMasters.filter((x) => x[childLevelIdKey] == null && x.isDirector === true); +} From 6a07841763daa6967bf811adcd890504a382d085 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 10 Mar 2026 18:12:58 +0700 Subject: [PATCH 144/178] =?UTF-8?q?fix=20bug=20=E0=B8=AD=E0=B8=AD=E0=B8=81?= =?UTF-8?q?=E0=B8=84=E0=B8=B3=E0=B8=AA=E0=B8=B1=E0=B9=88=E0=B8=87=E0=B8=A2?= =?UTF-8?q?=E0=B8=81=E0=B9=80=E0=B8=A5=E0=B8=B4=E0=B8=81=E0=B8=A5=E0=B8=B2?= =?UTF-8?q?=E0=B8=AD=E0=B8=AD=E0=B8=81=20#2183=20+=20api=20=E0=B9=81?= =?UTF-8?q?=E0=B8=81=E0=B9=89=E0=B9=84=E0=B8=82=E0=B9=80=E0=B8=9B=E0=B8=A5?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99=E0=B8=9C=E0=B8=B9=E0=B9=89?= =?UTF-8?q?=E0=B8=AA=E0=B8=A3=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B8=84=E0=B8=B3?= =?UTF-8?q?=E0=B8=AA=E0=B8=B1=E0=B9=88=E0=B8=87=20#1551?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/controllers/CommandController.ts | 62 +++++++++++++++++++++------- src/controllers/ProfileController.ts | 1 + src/services/CommandService.ts | 47 +++++++++++++++++++++ 4 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 src/services/CommandService.ts diff --git a/.gitignore b/.gitignore index e8ab8057..55e347d2 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +.claude \ No newline at end of file diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index fa0e7172..9ed83c08 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -102,6 +102,7 @@ import { import { PostRetireToExprofile } from "./ExRetirementController"; import { LeaveType } from "../entities/LeaveType"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; +import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; @Route("api/v1/org/command") @Tags("Command") @Security("bearerAuth") @@ -2488,6 +2489,35 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API แก้ไขเปลี่ยนผู้สร้างคำสั่ง + * @summary API แก้ไขเปลี่ยนผู้สร้างคำสั่ง + * @param {string} id Id คำสั่ง + */ + @Put("change-creator/{id}") + async ChangeCreator( + @Path() id: string, + @Body() req: { assignId: string; }, + @Request() request: RequestWithUser, + ) { + await new permission().PermissionUpdate(request, "COMMAND"); + const command = await this.commandRepository.findOne({ + where: { id: id } + }); + + if (!command) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลคำสั่งนี้"); + } + + command.createdUserId = req.assignId; + command.lastUpdateUserId = request.user.sub; + command.lastUpdateFullName = request.user.name; + command.lastUpdatedAt = new Date(); + + await this.commandRepository.save(command); + return new HttpSuccess(); + } + /** * API สร้างรายการ body คำสั่ง * @@ -3984,6 +4014,7 @@ export class CommandController extends Controller { .orgRootShortName ?? ""; } } + const today = new Date().setHours(0,0,0,0); await Promise.all( body.data.map(async (item) => { const profile = await this.profileRepository.findOne({ @@ -4003,17 +4034,16 @@ export class CommandController extends Controller { if (code && ["C-PM-08", "C-PM-17", "C-PM-18"].includes(code)) { removePostMasterAct(profile.id); } - //ออกคำสั่งยกเลิกลาออกต้องอัพเดทสถานะคำสั่งลาออกเป็น CANCEL + //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก else if (item.resignId && code && ["C-PM-41"].includes(code)) { - const commandRecive = await this.commandReciveRepository.findOne({ - select: ["commandId"], + const commandResign = await this.commandReciveRepository.findOne({ where: { refId: item.resignId }, + relations: { command: true }, }); - if (commandRecive && commandRecive.commandId) { - await this.commandRepository.update( - { id: commandRecive?.commandId }, - { status: "CANCEL" }, - ); + const executeDate = commandResign ? new Date(commandResign.command.commandExcecuteDate).setHours(0,0,0,0) : today; + if (_command.status != "REPORTED" + && (_command.status != "WAITING" || today < executeDate)) { + await reOrderCommandRecivesAndDelete(commandResign!.refId); } } let _commandYear = item.commandYear; @@ -4386,6 +4416,7 @@ export class CommandController extends Controller { .orgRootShortName ?? ""; } } + const today = new Date().setHours(0,0,0,0); await Promise.all( body.data.map(async (item) => { const profile = await this.profileEmployeeRepository.findOne({ @@ -4401,17 +4432,16 @@ export class CommandController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } const code = _command?.commandType?.code; - //ออกคำสั่งยกเลิกลาออกต้องอัพเดทสถานะคำสั่งลาออกเป็น CANCEL + //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก if (item.resignId && code && ["C-PM-42"].includes(code)) { - const commandRecive = await this.commandReciveRepository.findOne({ - select: ["commandId"], + const commandResign = await this.commandReciveRepository.findOne({ where: { refId: item.resignId }, + relations: { command: true }, }); - if (commandRecive && commandRecive.commandId) { - await this.commandRepository.update( - { id: commandRecive?.commandId }, - { status: "CANCEL" }, - ); + const executeDate = commandResign ? new Date(commandResign.command.commandExcecuteDate).setHours(0,0,0,0) : today; + if (_command.status != "REPORTED" + && (_command.status !== "WAITING" || today < executeDate)) { + await reOrderCommandRecivesAndDelete(commandResign!.id); } } let _commandYear = item.commandYear; diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index a9436648..141b1b82 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -9200,6 +9200,7 @@ export class ProfileController extends Controller { return { id: item.id, + keycloak: item.keycloak, prefix: item.prefix, rank: item.rank, firstName: item.firstName, diff --git a/src/services/CommandService.ts b/src/services/CommandService.ts new file mode 100644 index 00000000..ad9326b6 --- /dev/null +++ b/src/services/CommandService.ts @@ -0,0 +1,47 @@ +import { AppDataSource } from "../database/data-source"; +import { CommandRecive } from "../entities/CommandRecive"; +import { Command } from "../entities/Command"; + +/** + * เรียงลำดับผู้ได้รับคำสั่งใหม่หลังจากลบรายการ และอัพเดทสถานะคำสั่งถ้าไม่มีผู้ได้รับคำสั่งเหลือ + * @param refId refId ของผู้ได้รับคำสั่ง + * @param code ประเภทคำสั่ง + * @returns Promise + */ +export async function reOrderCommandRecivesAndDelete( + refId: string +): Promise { + const commandReciveRepo = AppDataSource.getRepository(CommandRecive); + const commandRepo = AppDataSource.getRepository(Command); + + // ค้นหาข้อมูลผู้ได้รับคำสั่งตาม refId + const commandRecive = await commandReciveRepo.findOne({ + where: { refId: refId }, + relations: { command: true}, + }); + + if (commandRecive == null) + return; + + const commandId = commandRecive.commandId; + // ลบตาม refId + await commandReciveRepo.delete(commandRecive.id); + + const commandReciveList = await commandReciveRepo.find({ + where: { commandId: commandId }, + order: { order: "ASC" }, + }); + // ลำดับผู้ได้รับคำสั่งใหม่ + if (commandReciveList.length > 0) { + await Promise.all( + commandReciveList.map(async (p, i) => { + p.order = i + 1; + await commandReciveRepo.save(p); + }) + ); + } else { + // ถ้าไม่มีผู้ได้รับคำสั่งเหลือเลย ให้ยกเลิกคำสั่ง + await commandRepo.update({ id: commandId }, { status: "CANCEL" }); + } + +} From 5ef5f7d4edd9f080288ac68efe4ab37f5109deeb Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 12 Mar 2026 14:53:45 +0700 Subject: [PATCH 145/178] fix bug #2183 --- src/controllers/CommandController.ts | 19 ++++++++++++------- src/services/CommandService.ts | 9 ++++----- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 9ed83c08..7bbdb9ef 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -4040,10 +4040,12 @@ export class CommandController extends Controller { where: { refId: item.resignId }, relations: { command: true }, }); - const executeDate = commandResign ? new Date(commandResign.command.commandExcecuteDate).setHours(0,0,0,0) : today; - if (_command.status != "REPORTED" - && (_command.status != "WAITING" || today < executeDate)) { - await reOrderCommandRecivesAndDelete(commandResign!.refId); + 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; @@ -4377,6 +4379,7 @@ export class CommandController extends Controller { let _posNumCodeSitAbb: string = ""; const _command = await this.commandRepository.findOne({ where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + relations: { commandType: true }, }); if (_command) { if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { @@ -4438,9 +4441,11 @@ export class CommandController extends Controller { where: { refId: item.resignId }, relations: { command: true }, }); - const executeDate = commandResign ? new Date(commandResign.command.commandExcecuteDate).setHours(0,0,0,0) : today; - if (_command.status != "REPORTED" - && (_command.status !== "WAITING" || today < executeDate)) { + 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); } } diff --git a/src/services/CommandService.ts b/src/services/CommandService.ts index ad9326b6..295d9ce8 100644 --- a/src/services/CommandService.ts +++ b/src/services/CommandService.ts @@ -4,20 +4,19 @@ import { Command } from "../entities/Command"; /** * เรียงลำดับผู้ได้รับคำสั่งใหม่หลังจากลบรายการ และอัพเดทสถานะคำสั่งถ้าไม่มีผู้ได้รับคำสั่งเหลือ - * @param refId refId ของผู้ได้รับคำสั่ง + * @param reciveId commandRecive.Id ของผู้ได้รับคำสั่ง * @param code ประเภทคำสั่ง * @returns Promise */ export async function reOrderCommandRecivesAndDelete( - refId: string + reciveId: string ): Promise { const commandReciveRepo = AppDataSource.getRepository(CommandRecive); const commandRepo = AppDataSource.getRepository(Command); - // ค้นหาข้อมูลผู้ได้รับคำสั่งตาม refId + // ค้นหาข้อมูลผู้ได้รับคำสั่งตาม reciveId const commandRecive = await commandReciveRepo.findOne({ - where: { refId: refId }, - relations: { command: true}, + where: { id: reciveId } }); if (commandRecive == null) From 6b3d1dc67b940bb9b847c9ebfb302cee846cab94 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 12 Mar 2026 18:01:15 +0700 Subject: [PATCH 146/178] =?UTF-8?q?API=20=E0=B8=A5=E0=B8=9A=E0=B8=82?= =?UTF-8?q?=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=B9=E0=B8=A5=E0=B8=81=E0=B8=B2?= =?UTF-8?q?=E0=B8=A3=E0=B8=9D=E0=B8=B6=E0=B8=81=E0=B8=AD=E0=B8=9A=E0=B8=A3?= =?UTF-8?q?=E0=B8=A1/=E0=B8=94=E0=B8=B9=E0=B8=87=E0=B8=B2=E0=B8=99=20?= =?UTF-8?q?=E0=B9=81=E0=B8=A5=E0=B8=B0=20=E0=B8=81=E0=B8=B2=E0=B8=A3?= =?UTF-8?q?=E0=B8=9E=E0=B8=B1=E0=B8=92=E0=B8=99=E0=B8=B2=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=E0=B8=9A=E0=B8=B8=E0=B8=84=E0=B8=84=E0=B8=A5=20IDP=20?= =?UTF-8?q?=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=9A=E0=B8=B8=E0=B8=84=E0=B8=84?= =?UTF-8?q?=E0=B8=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProfileDevelopmentEmployeeController.ts | 39 +++++++++ src/controllers/ProfileTrainingController.ts | 82 +++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/src/controllers/ProfileDevelopmentEmployeeController.ts b/src/controllers/ProfileDevelopmentEmployeeController.ts index 2f5a7715..d56ae484 100644 --- a/src/controllers/ProfileDevelopmentEmployeeController.ts +++ b/src/controllers/ProfileDevelopmentEmployeeController.ts @@ -27,6 +27,7 @@ import { import permission from "../interfaces/permission"; import { DevelopmentProject } from "../entities/DevelopmentProject"; import { In, Brackets } from "typeorm"; +import { setLogDataDiff } from "../interfaces/utils"; @Route("api/v1/org/profile-employee/development") @Tags("ProfileDevelopment") @Security("bearerAuth") @@ -273,6 +274,44 @@ export class ProfileDevelopmentEmployeeController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการพัฒนารายบุคคล IDP + * @summary API ลบข้อมูลการพัฒนารายบุคคล IDP + * @param developmentId คีย์การพัฒนารายบุคคล IDP + */ + @Patch("update-delete/{developmentId}") + public async updateIsDeletedTraining( + @Request() req: RequestWithUser, + @Path() developmentId: string, + ) { + const record = await this.developmentRepository.findOneBy({ id: developmentId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + const before = structuredClone(record); + const history = new ProfileDevelopmentHistory(); + const now = new Date(); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = now; + + Object.assign(history, { ...record, id: undefined }); + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = now; + + await Promise.all([ + this.developmentRepository.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + this.developmentHistoryRepository.save(history, { data: req }), + ]); + + return new HttpSuccess(); + } + @Delete("{developmentId}") public async deleteDevelopment(@Path() developmentId: string, @Request() req: RequestWithUser) { const _record = await this.developmentRepository.findOneBy({ id: developmentId }); diff --git a/src/controllers/ProfileTrainingController.ts b/src/controllers/ProfileTrainingController.ts index 0df7594f..a286d26a 100644 --- a/src/controllers/ProfileTrainingController.ts +++ b/src/controllers/ProfileTrainingController.ts @@ -23,6 +23,7 @@ import HttpError from "../interfaces/http-error"; import { ProfileTrainingHistory } from "../entities/ProfileTrainingHistory"; import { RequestWithUser } from "../middlewares/user"; import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; import { ProfileDevelopment } from "../entities/ProfileDevelopment"; @@ -33,11 +34,13 @@ import { In } from "typeorm"; @Security("bearerAuth") export class ProfileTrainingController extends Controller { private profileRepo = AppDataSource.getRepository(Profile); + private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); private trainingRepo = AppDataSource.getRepository(ProfileTraining); private trainingHistoryRepo = AppDataSource.getRepository(ProfileTrainingHistory); private developmentRepo = AppDataSource.getRepository(ProfileDevelopment); private developmentHistoryRepo = AppDataSource.getRepository(ProfileDevelopmentHistory); + @Get("user") public async getTrainingUser(@Request() request: { user: Record }) { const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); @@ -256,4 +259,83 @@ export class ProfileTrainingController extends Controller { return new HttpSuccess(); } + /** + * API ลบข้อมูลการฝึกอบรม/ดูงาน และ การพัฒนารายบุคคล IDP รายบุคคล + * @summary API ลบข้อมูลการฝึกอบรม/ดูงาน และ การพัฒนารายบุคคล IDP รายบุคคล + */ + @Post("delete-byId") + public async deleteById( + @Body() reqBody: { + type: string; + profileId: string; + developmentId: string; + }, + @Request() req: RequestWithUser + ) { + + const type = reqBody.type?.trim().toUpperCase(); + // 1. validate profile + if (type === "OFFICER") { + const profile = await this.profileRepo.findOneBy({ id: reqBody.profileId }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + } else { + const profile = await this.profileEmployeeRepo.findOneBy({ id: reqBody.profileId }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + } + + const profileField = type === "OFFICER" ? "profileId" : "profileEmployeeId"; + + // 2. หา ProfileTraining + const trainings = await this.trainingRepo.find({ + select: { id: true }, + where: { + developmentId: reqBody.developmentId, + [profileField]: reqBody.profileId, + }, + }); + + if (trainings.length > 0) { + const trainingIds = trainings.map(x => x.id); + + // 3. ลบ TrainingHistory + await this.trainingHistoryRepo.delete({ + profileTrainingId: In(trainingIds), + }); + + // 4. ลบ ProfileTraining + await this.trainingRepo.delete({ + id: In(trainingIds), + }); + } + + // 5. หา ProfileDevelopment + const developments = await this.developmentRepo.find({ + select: { id: true }, + where: { + kpiDevelopmentId: reqBody.developmentId, + [profileField]: reqBody.profileId, + }, + }); + + if (developments.length > 0) { + const devIds = developments.map(x => x.id); + + // 6. ลบ DevelopmentHistory + await this.developmentHistoryRepo.delete({ + kpiDevelopmentId: In(devIds), + }); + + // 7. ลบ ProfileDevelopment + await this.developmentRepo.delete({ + id: In(devIds), + }); + } + + return new HttpSuccess(); + } + } From 41f3c5ca5455c9179960d85b3c5eafa9d7aa77ac Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 12 Mar 2026 18:31:24 +0700 Subject: [PATCH 147/178] no message --- src/controllers/ProfileTrainingController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/ProfileTrainingController.ts b/src/controllers/ProfileTrainingController.ts index a286d26a..ac48f021 100644 --- a/src/controllers/ProfileTrainingController.ts +++ b/src/controllers/ProfileTrainingController.ts @@ -326,7 +326,7 @@ export class ProfileTrainingController extends Controller { // 6. ลบ DevelopmentHistory await this.developmentHistoryRepo.delete({ - kpiDevelopmentId: In(devIds), + profileDevelopmentId: In(devIds), }); // 7. ลบ ProfileDevelopment From 5d68a245a8466a9651160aa9b627279b1ae60005 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 13 Mar 2026 13:24:49 +0700 Subject: [PATCH 148/178] =?UTF-8?q?fix=20bug=20=E0=B8=9C=E0=B8=B9=E0=B9=89?= =?UTF-8?q?=E0=B8=94=E0=B8=B3=E0=B9=80=E0=B8=99=E0=B8=B4=E0=B8=99=E0=B8=81?= =?UTF-8?q?=E0=B8=B2=E0=B8=A3=E0=B9=81=E0=B8=AA=E0=B8=94=E0=B8=87=E0=B8=8B?= =?UTF-8?q?=E0=B9=89=E0=B8=B3=20#2355?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 135 ++++++++++++++------------- 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 7bbdb9ef..55f40896 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -2621,74 +2621,79 @@ export class CommandController extends Controller { await this.commandRepository.save(command); } // insert commandOperator - if (request.user.sub) { - const profile = await this.profileRepository.findOne({ - where: { keycloak: request.user.sub }, - relations: { - posLevel: true, - posType: true, - current_holders: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }, - }); - if (profile) { - const currentHolder = profile!.current_holders?.find( - (x) => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, - ); - - const posNo = - currentHolder != null && currentHolder.orgChild4 != null - ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild3 != null - ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild2 != null - ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild1 != null - ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder?.orgRoot != null - ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` - : null; - - const position = await this.positionRepository.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: currentHolder?.orgRevisionId, - current_holderId: profile!.id, + const checkCommandOperator = await this.commandOperatorRepository.findOne({ + where: { commandId: command.id, roleName: "เจ้าหน้าที่ดำเนินการ" }, + }); + if (!checkCommandOperator) { + if (request.user.sub) { + const profile = await this.profileRepository.findOne({ + where: { keycloak: request.user.sub }, + relations: { + posLevel: true, + posType: true, + current_holders: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, }, }, - order: { createdAt: "DESC" }, - relations: { posExecutive: true }, }); - const operator = Object.assign(new CommandOperator(), { - profileId: profile?.id, - prefix: profile?.prefix, - firstName: profile?.firstName, - lastName: profile?.lastName, - posNo: posNo, - posType: profile?.posType?.posTypeName ?? null, - posLevel: profile?.posLevel?.posLevelName ?? null, - position: position?.positionName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - roleName: "เจ้าหน้าที่ดำเนินการ", - orderNo: 1, - commandId: command.id, - createdUserId: request.user.sub, - createdFullName: request.user.name, - createdAt: now, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - lastUpdatedAt: now, - }); - await this.commandOperatorRepository.save(operator); + if (profile) { + const currentHolder = profile!.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + const posNo = + currentHolder != null && currentHolder.orgChild4 != null + ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild3 != null + ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild2 != null + ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild1 != null + ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder?.orgRoot != null + ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` + : null; + + const position = await this.positionRepository.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: currentHolder?.orgRevisionId, + current_holderId: profile!.id, + }, + }, + order: { createdAt: "DESC" }, + relations: { posExecutive: true }, + }); + const operator = Object.assign(new CommandOperator(), { + profileId: profile?.id, + prefix: profile?.prefix, + firstName: profile?.firstName, + lastName: profile?.lastName, + posNo: posNo, + posType: profile?.posType?.posTypeName ?? null, + posLevel: profile?.posLevel?.posLevelName ?? null, + position: position?.positionName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + roleName: "เจ้าหน้าที่ดำเนินการ", + orderNo: 1, + commandId: command.id, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + }); + await this.commandOperatorRepository.save(operator); + } } } From 66d8ba089d9df6b242e35c56a8767e19ce3d79a7 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 19 Mar 2026 10:16:09 +0700 Subject: [PATCH 149/178] addLogSequence --- src/controllers/CommandController.ts | 3 ++- src/controllers/ExRetirementController.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 55f40896..6116b453 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -4320,7 +4320,8 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - await PostRetireToExprofile( + + PostRetireToExprofile( profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index c64ccdeb..c17a232b 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -225,6 +225,19 @@ export async function PostRetireToExprofile( }, }); + // addLogSequence(request, { + // action: "request", + // status: "success", + // description: "connected", + // request: { + // method: "POST", + // url: url, + // payload: JSON.stringify(sendData), + // response: JSON.stringify(response.data.result), + // }, + // }); + + return res.data; } catch (error: any) { if (error.response?.status === 500 && retryCount < maxRetries - 1) { From ff752da7dd13640a5d8fae1b60d2013811f9c60f Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 19 Mar 2026 11:13:27 +0700 Subject: [PATCH 150/178] fix remove await and addLogSequence of exprofile system --- src/controllers/CommandController.ts | 279 +++++++++--------- src/controllers/ExRetirementController.ts | 27 +- src/controllers/ProfileController.ts | 5 +- src/controllers/ProfileEmployeeController.ts | 23 +- .../ProfileEmployeeTempController.ts | 23 +- 5 files changed, 186 insertions(+), 171 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 6116b453..f21473c4 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -235,7 +235,7 @@ export class CommandController extends Controller { ? _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 null` : "1=1", { child1: _data.child1, @@ -313,7 +313,7 @@ export class CommandController extends Controller { status == null || status == undefined || status == "" ? null : status.trim().toLocaleUpperCase() == "NEW" || - status.trim().toLocaleUpperCase() == "DRAFT" + status.trim().toLocaleUpperCase() == "DRAFT" ? ["NEW", "DRAFT"] : [status.trim().toLocaleUpperCase()], }, @@ -814,8 +814,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -858,8 +858,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -902,8 +902,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: [commandRecive.refId], }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); const commandId = commandRecive.commandId; await this.commandReciveRepository.delete(commandRecive.id); @@ -1187,8 +1187,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); await this.commandReciveRepository.delete({ commandId: command.id }); command.status = "CANCEL"; @@ -1253,8 +1253,8 @@ export class CommandController extends Controller { .PostData(request, path + "/delete", { refIds: command.commandRecives.map((x) => x.refId), }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); await this.commandSendCCRepository.delete({ commandSendId: In(commandSend.map((x) => x.id)) }); await this.commandReciveRepository.delete({ commandId: command.id }); @@ -1407,11 +1407,11 @@ export class CommandController extends Controller { let profiles = command && command.commandRecives.length > 0 ? command.commandRecives - .filter((x) => x.profileId != null) - .map((x) => ({ - receiverUserId: x.profileId, - notiLink: "", - })) + .filter((x) => x.profileId != null) + .map((x) => ({ + receiverUserId: x.profileId, + notiLink: "", + })) : []; const msgNoti = { @@ -1443,8 +1443,8 @@ export class CommandController extends Controller { refIds: command.commandRecives.filter((x) => x.refId != null).map((x) => x.refId), status: "WAITING", }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); await this.commandRepository.save(command); } else { const path = commandTypePath(command.commandType.code); @@ -1581,7 +1581,7 @@ export class CommandController extends Controller { ); await this.profileRepository.save(profiles); } - } catch { } + } catch {} type = "EMPLOYEE"; try { @@ -1613,7 +1613,7 @@ export class CommandController extends Controller { ); await this.profileEmployeeRepository.save(profiles); } - } catch { } + } catch {} return new HttpSuccess(); } @@ -1677,7 +1677,7 @@ export class CommandController extends Controller { }), ); } - } catch { } + } catch {} type = "EMPLOYEE"; try { @@ -1732,7 +1732,7 @@ export class CommandController extends Controller { }), ); } - } catch { } + } catch {} return new HttpSuccess(); } @@ -1945,7 +1945,7 @@ export class CommandController extends Controller { .then((x) => { res = x; }) - .catch((x) => { }); + .catch((x) => {}); } let _command; @@ -2023,76 +2023,76 @@ export class CommandController extends Controller { profile?.current_holders.length == 0 ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild4 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4.orgChild4ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) != - null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3 != null + null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild3 != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3.orgChild3ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2 != null - ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2.orgChild2ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` - : profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - ) != null && - profile?.current_holders.find( - (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgChild1 != null - ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1.orgChild1ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` - : profile?.current_holders.find( (x) => x.orgRevisionId == orgRevisionActive?.id, ) != null && + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) + ?.orgChild2 != null + ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2.orgChild2ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` + : profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && profile?.current_holders.find( (x) => x.orgRevisionId == orgRevisionActive?.id, - )?.orgRoot != null + )?.orgChild1 != null + ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1.orgChild1ShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` + : profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + ) != null && + profile?.current_holders.find( + (x) => x.orgRevisionId == orgRevisionActive?.id, + )?.orgRoot != null ? `${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot.orgRootShortName} ${profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.posMasterNo}` : null; const root = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgRoot; + ?.orgRoot; const child1 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild1; + ?.orgChild1; const child2 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild2; + ?.orgChild2; const child3 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild3; + ?.orgChild3; const child4 = profile?.current_holders == null || - profile?.current_holders.length == 0 || - profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + profile?.current_holders.length == 0 || + profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : profile?.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild4; + ?.orgChild4; let _root = root?.orgRootName; let _child1 = child1?.orgChild1Name; @@ -2153,10 +2153,10 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.isLeave == false ? (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root) + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root) : orgLeave : profileTemp.org, fullName: `${x.prefix}${x.firstName} ${x.lastName}`, @@ -2171,8 +2171,8 @@ export class CommandController extends Controller { commandCode != "C-PM-21" ? profile?.posType && profile?.posLevel ? Extension.ToThaiNumber( - `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, - ) + `${profile?.posType.posTypeShortName} ${profile?.posLevel.posLevelName}`, + ) : "-" : Extension.ToThaiNumber(profileTemp.posLevel), posNo: @@ -2186,19 +2186,19 @@ export class CommandController extends Controller { ? Extension.ToThaiNumber(Extension.ToThaiShortDate_monthYear(profile?.dateRetire)) : profile?.birthDate && commandCode == "C-PM-21" ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear( - new Date( - profile.birthDate.getFullYear() + 60, - profile.birthDate.getMonth(), - profile.birthDate.getDate(), + Extension.ToThaiShortDate_monthYear( + new Date( + profile.birthDate.getFullYear() + 60, + profile.birthDate.getMonth(), + profile.birthDate.getDate(), + ), ), - ), - ) + ) : "-", dateExecute: command.commandExcecuteDate ? Extension.ToThaiNumber( - Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), - ) + Extension.ToThaiShortDate_monthYear(command.commandExcecuteDate), + ) : "-", remark: x.remarkVertical ? x.remarkVertical : "-", }; @@ -2299,7 +2299,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => { }); + .catch(() => {}); let issue = command.isBangkok == "OFFICE" @@ -2357,15 +2357,15 @@ export class CommandController extends Controller { operators: operators.length > 0 ? operators.map((x) => ({ - fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, - roleName: x.roleName, - })) + fullName: `${x.prefix ?? ""}${x.firstName ?? ""} ${x.lastName ?? ""}`, + roleName: x.roleName, + })) : [ - { - fullName: "", - roleName: "เจ้าหน้าที่ดำเนินการ", - }, - ], + { + fullName: "", + roleName: "เจ้าหน้าที่ดำเนินการ", + }, + ], }, }); } @@ -2497,12 +2497,12 @@ export class CommandController extends Controller { @Put("change-creator/{id}") async ChangeCreator( @Path() id: string, - @Body() req: { assignId: string; }, + @Body() req: { assignId: string }, @Request() request: RequestWithUser, ) { await new permission().PermissionUpdate(request, "COMMAND"); const command = await this.commandRepository.findOne({ - where: { id: id } + where: { id: id }, }); if (!command) { @@ -2513,7 +2513,7 @@ export class CommandController extends Controller { command.lastUpdateUserId = request.user.sub; command.lastUpdateFullName = request.user.name; command.lastUpdatedAt = new Date(); - + await this.commandRepository.save(command); return new HttpSuccess(); } @@ -2704,8 +2704,8 @@ export class CommandController extends Controller { refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), status: "REPORT", }) - .then(async (res) => { }) - .catch(() => { }); + .then(async (res) => {}) + .catch(() => {}); let order = command.commandRecives == null || command.commandRecives.length <= 0 ? 0 @@ -3478,27 +3478,27 @@ export class CommandController extends Controller { ? x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName : x.orgChild3 == null ? x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName - : x.orgChild4 == null - ? x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + "\n" + x.orgChild1.orgChild1Name + "\n" + x.orgRoot.orgRootName + : x.orgChild4 == null + ? x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName : x.orgChild4.orgChild4Name + - "\n" + - x.orgChild3.orgChild3Name + - "\n" + - x.orgChild2.orgChild2Name + - "\n" + - x.orgChild1.orgChild1Name + - "\n" + - x.orgRoot.orgRootName, + "\n" + + x.orgChild3.orgChild3Name + + "\n" + + x.orgChild2.orgChild2Name + + "\n" + + x.orgChild1.orgChild1Name + + "\n" + + x.orgRoot.orgRootName, positionName: x?.current_holder.position ?? _null, profileId: x?.current_holder.id ?? _null, }); @@ -4019,7 +4019,7 @@ export class CommandController extends Controller { .orgRootShortName ?? ""; } } - const today = new Date().setHours(0,0,0,0); + const today = new Date().setHours(0, 0, 0, 0); await Promise.all( body.data.map(async (item) => { const profile = await this.profileRepository.findOne({ @@ -4045,11 +4045,14 @@ export class CommandController extends Controller { where: { refId: item.resignId }, relations: { command: true }, }); - const executeDate = commandResign - ? new Date(commandResign.command.commandExcecuteDate).setHours(0,0,0,0) + const executeDate = commandResign + ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) : today; - if (commandResign && _command.status !== "REPORTED" && - (_command.status !== "WAITING" || today < executeDate)) { + if ( + commandResign && + _command.status !== "REPORTED" && + (_command.status !== "WAITING" || today < executeDate) + ) { await reOrderCommandRecivesAndDelete(commandResign!.id); } } @@ -4320,8 +4323,9 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - + PostRetireToExprofile( + req, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", @@ -4425,7 +4429,7 @@ export class CommandController extends Controller { .orgRootShortName ?? ""; } } - const today = new Date().setHours(0,0,0,0); + const today = new Date().setHours(0, 0, 0, 0); await Promise.all( body.data.map(async (item) => { const profile = await this.profileEmployeeRepository.findOne({ @@ -4447,11 +4451,14 @@ export class CommandController extends Controller { where: { refId: item.resignId }, relations: { command: true }, }); - const executeDate = commandResign - ? new Date(commandResign.command.commandExcecuteDate).setHours(0,0,0,0) + const executeDate = commandResign + ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) : today; - if (commandResign && _command.status !== "REPORTED" && - (_command.status !== "WAITING" || today < executeDate)) { + if ( + commandResign && + _command.status !== "REPORTED" && + (_command.status !== "WAITING" || today < executeDate) + ) { await reOrderCommandRecivesAndDelete(commandResign!.id); } } @@ -4572,7 +4579,8 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - await PostRetireToExprofile( + PostRetireToExprofile( + req, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", @@ -4842,7 +4850,8 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - await PostRetireToExprofile( + PostRetireToExprofile( + req, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", @@ -5473,7 +5482,8 @@ export class CommandController extends Controller { ? `${profile.posLevel?.posLevelName}` : `${profile.posType?.posTypeName} ${profile.posLevel?.posLevelName}`; - await PostRetireToExprofile( + PostRetireToExprofile( + req, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", @@ -6126,26 +6136,26 @@ export class CommandController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild4 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; @@ -6249,7 +6259,8 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - await PostRetireToExprofile( + PostRetireToExprofile( + req, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", @@ -6976,8 +6987,8 @@ export class CommandController extends Controller { prefix: avatar, fileName: fileName, }) - .then(() => { }) - .catch(() => { }); + .then(() => {}) + .catch(() => {}); } } }), @@ -8088,7 +8099,7 @@ export class CommandController extends Controller { .then(async (res) => { _command = res; }) - .catch(() => { }); + .catch(() => {}); let issue = command.isBangkok == "OFFICE" diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index c17a232b..128cb4d1 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -14,6 +14,7 @@ import { } from "tsoa"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; +import { addLogSequence } from "../interfaces/utils"; interface CachedToken { token: string; @@ -171,6 +172,7 @@ async function getToken(ClientID: string, ClientSecret: string): Promise // function post retire data to exprofile system export async function PostRetireToExprofile( + request: any, citizenID: string, prefix: string, firstName: string, @@ -225,19 +227,6 @@ export async function PostRetireToExprofile( }, }); - // addLogSequence(request, { - // action: "request", - // status: "success", - // description: "connected", - // request: { - // method: "POST", - // url: url, - // payload: JSON.stringify(sendData), - // response: JSON.stringify(response.data.result), - // }, - // }); - - return res.data; } catch (error: any) { if (error.response?.status === 500 && retryCount < maxRetries - 1) { @@ -245,6 +234,18 @@ export async function PostRetireToExprofile( retryCount++; continue; } + + addLogSequence(request, { + action: "request", + status: "error", + description: "unconnected to exprofile api", + request: { + method: "POST", + url: API_URL_BANGKOK + "/importData", + response: JSON.stringify(error), + }, + }); + throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้"); } } diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 141b1b82..896717c8 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -3011,7 +3011,7 @@ export class ProfileController extends Controller { }; const orgRoot = await this.orgRootRepo.findOne({ select: { id: true, isDeputy: true }, - where: { + where: { id: Not(posMaster.orgRootId ?? ""), isDeputy: true, orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, @@ -11013,7 +11013,8 @@ export class ProfileController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - await PostRetireToExprofile( + PostRetireToExprofile( + request, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 72835a48..43dbd8f3 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -2290,27 +2290,27 @@ export class ProfileEmployeeController extends Controller { @Get("history/user") async getHistoryProfileByUser(@Request() request: RequestWithUser) { const profile = await this.profileRepo.findOne({ - where: { keycloak: request.user.sub } + where: { keycloak: request.user.sub }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const profileHistory = await this.profileHistoryRepo.find({ where: { profileEmployeeId: profile.id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); - + if (profileHistory.length == 0) { await this.profileHistoryRepo.save( Object.assign(new ProfileEmployeeHistory(), { ...profile, birthDateOld: profile?.birthDate, profileEmployeeId: profile.id, - id: undefined + id: undefined, }), ); const firstRecord = await this.profileHistoryRepo.find({ where: { profileEmployeeId: profile.id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); return new HttpSuccess(firstRecord); } @@ -3207,27 +3207,27 @@ export class ProfileEmployeeController extends Controller { async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) { //await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", id); ไม่แน่ใจEMPปิดไว้ก่อน; const profile = await this.profileRepo.findOne({ - where: { id: id } + where: { id: id }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const profileHistory = await this.profileHistoryRepo.find({ where: { profileEmployeeId: id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); - + if (profileHistory.length == 0) { await this.profileHistoryRepo.save( Object.assign(new ProfileEmployeeHistory(), { ...profile, birthDateOld: profile?.birthDate, profileEmployeeId: id, - id: undefined + id: undefined, }), ); const firstRecord = await this.profileHistoryRepo.find({ where: { profileEmployeeId: id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); return new HttpSuccess(firstRecord); } @@ -5450,7 +5450,8 @@ export class ProfileEmployeeController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - await PostRetireToExprofile( + PostRetireToExprofile( + request, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index 72dd1c43..7930b872 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -1295,27 +1295,27 @@ export class ProfileEmployeeTempController extends Controller { @Get("history/user") async getHistoryProfileByUser(@Request() request: RequestWithUser) { const profile = await this.profileRepo.findOne({ - where: { keycloak: request.user.sub } + where: { keycloak: request.user.sub }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const profileHistory = await this.profileHistoryRepo.find({ where: { profileEmployeeId: profile.id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); - + if (profileHistory.length == 0) { await this.profileHistoryRepo.save( Object.assign(new ProfileEmployeeHistory(), { ...profile, birthDateOld: profile?.birthDate, profileEmployeeId: profile.id, - id: undefined + id: undefined, }), ); const firstRecord = await this.profileHistoryRepo.find({ where: { profileEmployeeId: profile.id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); return new HttpSuccess(firstRecord); } @@ -1828,27 +1828,27 @@ export class ProfileEmployeeTempController extends Controller { async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) { // await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP");//ไม่แน่ใจTEMPปิดไว้ก่อน const profile = await this.profileRepo.findOne({ - where: { id: id } + where: { id: id }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const profileHistory = await this.profileHistoryRepo.find({ where: { profileEmployeeId: id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); - + if (profileHistory.length == 0) { await this.profileHistoryRepo.save( Object.assign(new ProfileEmployeeHistory(), { ...profile, birthDateOld: profile?.birthDate, profileEmployeeId: id, - id: undefined + id: undefined, }), ); const firstRecord = await this.profileHistoryRepo.find({ where: { profileEmployeeId: id }, - order: { createdAt: "ASC" } + order: { createdAt: "ASC" }, }); return new HttpSuccess(firstRecord); } @@ -3606,7 +3606,8 @@ export class ProfileEmployeeTempController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } - await PostRetireToExprofile( + PostRetireToExprofile( + request, profile.citizenId ?? "", profile.prefix ?? "", profile.firstName ?? "", From bc83a2bc4040d024758a02a9b6def6b3b30cba6e Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 20 Mar 2026 11:02:53 +0700 Subject: [PATCH 151/178] =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B8=82?= =?UTF-8?q?=E0=B9=89=E0=B8=AD=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1/?= =?UTF-8?q?=E0=B8=95=E0=B8=B1=E0=B8=94=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1?= =?UTF-8?q?=E0=B8=B9=E0=B8=A5=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=87=E0=B8=B2?= =?UTF-8?q?=E0=B8=99=20=E0=B8=97=E0=B8=9B=E0=B8=AD.=E0=B8=AA=E0=B8=B2?= =?UTF-8?q?=E0=B8=A1=E0=B8=B1=E0=B8=8D=20#2274?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileController.ts | 364 ++++++++++++++----- src/controllers/ProfileEmployeeController.ts | 194 +++++++--- src/services/CommandService.ts | 100 +++++ 3 files changed, 517 insertions(+), 141 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 141b1b82..885c73e5 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -90,6 +90,7 @@ import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { CreatePosMasterHistoryOfficer, getTopDegrees } from "../services/PositionService"; import { ProfileLeaveService } from "../services/ProfileLeaveService"; import { PostRetireToExprofile } from "./ExRetirementController"; +import { getPosNumCodeSit } from "../services/CommandService"; @Route("api/v1/org/profile") @Tags("Profile") @Security("bearerAuth") @@ -945,6 +946,7 @@ export class ProfileController extends Controller { public async getKk1new(@Path() id: string, @Request() req: RequestWithUser) { const profiles = await this.profileRepo.findOne({ relations: [ + "posLevel", "currentSubDistrict", "currentDistrict", "currentProvince", @@ -1025,6 +1027,13 @@ export class ProfileController extends Controller { order: { lastUpdatedAt: "DESC" }, }); + const posMasterId = + profiles.current_holders == null || + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + ? null + : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.id; + const root = profiles.current_holders == null || profiles.current_holders.length == 0 || @@ -1085,12 +1094,7 @@ export class ProfileController extends Controller { certificateType: item.certificateType ?? null, issuer: item.issuer ?? null, certificateNo: item.certificateNo ? Extension.ToThaiNumber(item.certificateNo) : null, - issueDate: item.issueDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) - : "", - expireDate: item.expireDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) - : "", + detail: Extension.ToThaiNumber(`${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim()), issueToExpireDate: item.issueDate ? item.expireDate ? Extension.ToThaiNumber( @@ -1106,13 +1110,12 @@ export class ProfileController extends Controller { certificateType: "-", issuer: "-", certificateNo: "-", - issueDate: "-", - expireDate: "-", + detail: "-", issueToExpireDate: "-", }, ]; const training_raw = await this.trainingRepository.find({ - select: ["place", "department", "name", "duration", "isDeleted"], + select: ["place", "department", "name", "duration", "isDeleted", "startDate", "endDate"], where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); @@ -1123,6 +1126,7 @@ export class ProfileController extends Controller { degree: item.name ? Extension.ToThaiNumber(item.name) : "", place: item.place ? Extension.ToThaiNumber(item.place) : "", duration: item.duration ? Extension.ToThaiNumber(item.duration) : "", + date: Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`) })) : [ { @@ -1130,6 +1134,7 @@ export class ProfileController extends Controller { degree: "-", place: "-", duration: "-", + date: "-" }, ]; @@ -1171,13 +1176,15 @@ export class ProfileController extends Controller { date: item.isDate ? `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)) : ""}` : `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.startDate))) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.endDate))) : ""}`, - degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", + degree: `${item.degree ?? ""} ${item.field ?? ""}`.trim(), + level: item.educationLevel })) : [ { institute: "-", date: "-", degree: "-", + level: "-" }, ]; const salary_raw = await this.salaryRepo.find({ @@ -1297,8 +1304,8 @@ export class ProfileController extends Controller { section: item.section ? Extension.ToThaiNumber(item.section) : "", page: item.page ? Extension.ToThaiNumber(item.page) : "", refCommandDate: item.refCommandDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) - : "", + ? Extension.ToThaiNumber(`ราชกิจจานุเบกษา เล่มที่ ${item.volume ?? "-"} ตอนที่ ${item.section ?? "-"} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`) + : "-", note: item.note ? Extension.ToThaiNumber(item.note) : "", })) : [ @@ -1314,6 +1321,7 @@ export class ProfileController extends Controller { section: "-", page: "-", refCommandDate: "-", + note: "-" }, ]; @@ -1517,6 +1525,7 @@ export class ProfileController extends Controller { positionSalaryAmount: item.positionSalaryAmount ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) : "", + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1529,75 +1538,211 @@ export class ProfileController extends Controller { posLevel: "-", amount: "-", positionSalaryAmount: "-", + refDoc: "-" }, ]; + // ประวัติพ้นจากราชการ + const retireCodes = ['C-PM-12','C-PM-13','C-PM-17','C-PM-18','C-PM-19','C-PM-20','C-PM-23','C-PM-43']; + + let retires = []; + let inRetirePeriod = false; + let currentRetire = null; + + for (let i = 0; i < position_raw.length; i++) { + const item = position_raw[i]; + + if (retireCodes.includes(item.commandCode)) { + // เริ่มพ้นจากราชการ + currentRetire = { + commandName: item.commandName ?? "-", + startDate: item.commandDateAffect + }; + inRetirePeriod = true; + } + else if (inRetirePeriod && currentRetire) { + // เจอคำสั่งถัดไปที่ไม่ใช่การพ้นจากราชการ = วันกลับเข้า + const endDate = item.commandDateAffect; + let daysCount = 0; + + if (currentRetire.startDate && endDate) { + const start = new Date(currentRetire.startDate); + const end = new Date(endDate); + daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + } + + const startDateStr = currentRetire.startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(currentRetire.startDate)) + : "-"; + const endDateStr = endDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) + : "-"; + + retires.push({ + date: `${startDateStr} - ${endDateStr}`, + detail: currentRetire.commandName, + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + }); + + inRetirePeriod = false; + currentRetire = null; + } + } + + // กรณีไม่มีข้อมูล + if (retires.length === 0) { + retires.push({ date: "-", detail: "-", day: "-" }); + } + + // รักษาการ const actposition_raw = await this.profileActpositionRepo.find({ - select: ["dateStart", "dateEnd", "position", "isDeleted"], - where: { profileId: id, isDeleted: false }, + select: ["dateStart", "profileId", "dateEnd", "position", "commandId", "isDeleted"], + where: { profileId: id, status: true, isDeleted: false }, order: { createdAt: "ASC" }, - }); + }); + let _actpositions = []; + if (actposition_raw.length > 0){ + for (const item of actposition_raw) { + let _commandTypename: string = "รักษาการในตำแหน่ง"; + let _document: string = "-"; + let _org: string = "-"; + + if (item.commandId) { + const { + posNumCodeSitAbb, + commandTypeName, + commandNo, + commandYear, + commandExcecuteDate, + } = await getPosNumCodeSit(item.commandId, "REPORTED"); + + _document = Extension.ToThaiNumber( + `คำสั่ง ${posNumCodeSitAbb} + ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} + ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))}` + ) + _commandTypename = commandTypeName; + } + // ค้นหาหน่วยงานที่รักษาการ + const _posmasterAct = await this.posMasterActRepository.findOne({ + where: { + posMasterChildId: posMasterId ?? "", + statusReport: "DONE" + } + }); + if (_posmasterAct) { + const _posMater = await this.posMasterRepo.findOne({ + where: { + id: _posmasterAct.posMasterId + }, + relations:{ + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + } + }); + let child4 = _posMater?.orgChild4?.orgChild4Name ?? "" + let child3 = _posMater?.orgChild3?.orgChild3Name ?? "" + let child2 = _posMater?.orgChild2?.orgChild2Name ?? "" + let child1 = _posMater?.orgChild1?.orgChild1Name ?? "" + let root = _posMater?.orgRoot?.orgRootName ?? "" + _org = `${child4} ${child3} ${child2} ${child1} ${root}`.trim(); + } + + _actpositions.push({ + data: + item.dateStart && item.dateEnd + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) + : item.dateStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) + : item.dateEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) + : "", + position: item.position ? Extension.ToThaiNumber(item.position) : "", + commandName: _commandTypename, + agency: _org, + note: _posmasterAct && _posmasterAct?.posMasterOrder + ? Extension.ToThaiNumber(_posmasterAct?.posMasterOrder.toString()) + : "-", + document: _document, + }); + } + } + else { + _actpositions.push({ + date: "-", + position: "-", + commandName: "-", + agency: "-", + note: "-", + document: "-", + }); + } + // ช่วยราชการ const assistance_raw = await this.profileAssistanceRepository.find({ select: ["dateStart", "dateEnd", "commandName", "agency", "document", "isDeleted"], - where: { profileId: id, isDeleted: false }, + where: { profileId: id, status: "PENDING", isDeleted: false }, order: { createdAt: "ASC" }, }); + let _assistances = [] + if (assistance_raw.length > 0) { + for (const item of assistance_raw) { + let _commandTypename: string = item.commandName ?? "ให้ช่วยราชการ"; + let _document: string = "-"; + let _org: string = item.agency ? Extension.ToThaiNumber(item.agency) : "-"; + + if (item.commandId) { + const { + posNumCodeSitAbb, + commandTypeName, + commandNo, + commandYear, + commandExcecuteDate, + } = await getPosNumCodeSit(item.commandId); + + _document = Extension.ToThaiNumber(` + คำสั่ง ${posNumCodeSitAbb} + ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} + ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))} + `) + _commandTypename = commandTypeName; + } + _assistances.push({ + data: + item.dateStart && item.dateEnd + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) + : item.dateStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) + : item.dateEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) + : "", + position: "-", // รักษาการปัจจุบันไม่มีฟิลด์ตำแหน่ง + commandName: _commandTypename, + agency: _org, + note: "-", + document: _document, + }); + } + } + else { + _assistances.push({ + date: "-", + position: "-", + commandName: "-", + agency: "-", + note: "-", + document: "-", + }); + } + // Merge รักษาการ และ ช่วยราชาร + const actposition = [..._actpositions, ..._assistances]; - const _actposition = - actposition_raw.length > 0 - ? actposition_raw.map((item) => ({ - date: - item.dateStart && item.dateEnd - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) - : item.dateStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) - : item.dateEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) - : "", - position: item.position ? Extension.ToThaiNumber(item.position) : "", - commandName: "รักษาการในตำแหน่ง", - agency: "", - document: "", - })) - : [ - { - date: "-", - position: "-", - commandName: "-", - agency: "-", - document: "-", - }, - ]; - const _assistance = - assistance_raw.length > 0 - ? assistance_raw.map((item) => ({ - date: - item.dateStart && item.dateEnd - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) - : item.dateStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) - : item.dateEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) - : "", - position: "", - commandName: item.commandName ? Extension.ToThaiNumber(item.commandName) : "", - agency: item.agency ? Extension.ToThaiNumber(item.agency) : "", - document: item.document ? Extension.ToThaiNumber(item.document) : "", - })) - : [ - { - date: "-", - position: "-", - commandName: "-", - agency: "-", - document: "-", - }, - ]; - const actposition = [..._actposition, ..._assistance]; const duty_raw = await this.dutyRepository.find({ where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, @@ -1615,15 +1760,21 @@ export class ProfileController extends Controller { : item.dateEnd ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) : "", - refCommandDate: item.refCommandDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) - : "", - refCommandNo: item.refCommandNo ? Extension.ToThaiNumber(item.refCommandNo) : "", + type: "-", + detail: Extension.ToThaiNumber(item.detail), + agency: "-", + refCommandNo: item.refCommandNo + ? item.refCommandDate + ? Extension.ToThaiNumber(`${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`) + : Extension.ToThaiNumber(item.refCommandNo) + : "-", })) : [ { date: "-", - refCommandDate: "-", + type: "-", + detail: "-", + agency: "-", refCommandNo: "-", }, ]; @@ -1635,7 +1786,9 @@ export class ProfileController extends Controller { assessments_raw.length > 0 ? assessments_raw.map((item) => ({ year: item.year ? Extension.ToThaiNumber((parseInt(item.year) + 543).toString()) : "", - period: item.period && item.period == "APR" ? "เมษายน" : "ตุลาคม", + period: item.period && item.period == "APR" + ? Extension.ToThaiNumber(`1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`) + : Extension.ToThaiNumber(`1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`), point1: item.point1 ? Extension.ToThaiNumber(item.point1.toString()) : "", point1Total: item.point1Total ? Extension.ToThaiNumber(item.point1Total.toString()) @@ -1644,18 +1797,20 @@ export class ProfileController extends Controller { point2Total: item.point2Total ? Extension.ToThaiNumber(item.point2Total.toString()) : "", - pointSum: item.pointSum ? Extension.ToThaiNumber(item.pointSum.toString()) : "", + pointSum: item.pointSum + ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) + : "", pointSumTh: item.pointSum ? Extension.textPoint(item.pointSum) : "", level: item.pointSum < 60.0 - ? "ต้องปรับปรุง (คะแนนต่ำกว่าร้อยละ ๖๐.๐๐)" + ? "ต้องปรับปรุง" : item.pointSum <= 69.99 && item.pointSum >= 60.0 - ? "พอใช้ (คะแนนร้อยละ ๖๐.๐๐ - ๖๙.๙๙)" + ? "พอใช้" : item.pointSum <= 79.99 && item.pointSum >= 70.0 - ? "ดี (คะแนนร้อยละ ๗๐.๐๐ - ๗๙.๙๙)" + ? "ดี" : item.pointSum <= 89.99 && item.pointSum >= 80.0 - ? "ดีมาก (คะแนนร้อยละ ๘๐.๐๐ - ๘๙.๙๙)" - : "ดีเด่น (คะแนนร้อยละ ๙๐.๐๐ ขึ้นไป)", + ? "ดีมาก" + : "ดีเด่น", })) : [ { @@ -1711,6 +1866,7 @@ export class ProfileController extends Controller { ? Extension.ToThaiNumber(item.positionCee) : null, amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1721,6 +1877,7 @@ export class ProfileController extends Controller { position: "-", posLevel: "-", amount: "-", + refDoc: "-" }, ]; @@ -1775,20 +1932,41 @@ export class ProfileController extends Controller { }) .catch(() => {}); if (!portfolios) { - portfolios = [{ name: "-", year: "-" }]; + portfolios = [{ name: "-", year: "-", position: "-" }]; } else { portfolios = portfolios.map((x: any) => ({ name: x.name ? Extension.ToThaiNumber(x.name) : "-", year: x.year ? Extension.ToThaiNumber(x.year.toString()) : "-", + position: `${x.position ?? "-"}` })); } - + const org = + (_child4 == null ? "" : _child4 + " ") + + (_child3 == null ? "" : _child3 + " ") + + (_child2 == null ? "" : _child2 + " ") + + (_child1 == null ? "" : _child1 + " ") + + (_root == null ? "" : _root).trim() + const _position = profiles?.position != null ? + profiles?.posLevel != null + ? `${profiles.position}${profiles.posLevel.posLevelName}` + : profiles.position + : "" + const ocAssistance = await this.profileAssistanceRepository.findOne({ + select: ["agency", "profileId", "commandName", "status", "isDeleted"], + where: { + profileId: id, + commandName: "ให้ช่วยราชการ", + status: "PENDING", + isDeleted: false, }, + order: { createdAt: "ASC" }, + }); const data = { fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, prefix: profiles?.prefix != null ? profiles.prefix : "", firstName: profiles?.firstName != null ? profiles.firstName : "", lastName: profiles?.lastName != null ? profiles.lastName : "", - position: profiles?.position != null ? profiles.position : "", + position: _position, + posAssistance: ocAssistance ? "-" : _position, // ปัจจุบันช่วยราชการยังไม่มีเก็บฟิลด์ตำแหน่ง amount: profiles?.amount != null ? Extension.ToThaiNumber(profiles.amount.toLocaleString()) : "", positionSalaryAmount: @@ -1804,12 +1982,13 @@ export class ProfileController extends Controller { ? Extension.ToThaiNumber(profiles.amountSpecial.toLocaleString()) : "", salarySum: sum, - ocFullPath: - (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root), + ocFullPath: org, + ocAssistance: ocAssistance?.agency ?? org, + root: _root == null ? "" : _root, + agency: (_child4 == null ? "" : _child4 + " ") + + (_child3 == null ? "" : _child3 + " ") + + (_child2 == null ? "" : _child2 + " ") + + (_child1 == null ? "" : _child1).trim(), birthDate: profiles?.birthDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(profiles.birthDate)) : "", @@ -1939,6 +2118,7 @@ export class ProfileController extends Controller { profileAbility, otherIncome, portfolios, + retires }; return new HttpSuccess({ diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 72835a48..30743001 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -941,6 +941,7 @@ export class ProfileEmployeeController extends Controller { public async getKk1new(@Path() id: string, @Request() req: RequestWithUser) { const profiles = await this.profileRepo.findOne({ relations: [ + "posLevel", "currentSubDistrict", "currentDistrict", "currentProvince", @@ -1021,6 +1022,13 @@ export class ProfileEmployeeController extends Controller { order: { lastUpdatedAt: "DESC" }, }); + const posMasterId = + profiles.current_holders == null || + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + ? null + : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.id; + const root = profiles.current_holders == null || profiles.current_holders.length == 0 || @@ -1081,12 +1089,7 @@ export class ProfileEmployeeController extends Controller { certificateType: item.certificateType ?? null, issuer: item.issuer ?? null, certificateNo: item.certificateNo ? Extension.ToThaiNumber(item.certificateNo) : null, - issueDate: item.issueDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) - : "", - expireDate: item.expireDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) - : "", + detail: Extension.ToThaiNumber(`${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim()), issueToExpireDate: item.issueDate ? item.expireDate ? Extension.ToThaiNumber( @@ -1102,13 +1105,12 @@ export class ProfileEmployeeController extends Controller { certificateType: "-", issuer: "-", certificateNo: "-", - issueDate: "-", - expireDate: "-", + detail: "-", issueToExpireDate: "-", }, ]; const training_raw = await this.trainingRepository.find({ - select: ["place", "department", "name", "duration", "isDeleted"], + select: ["place", "department", "name", "duration", "isDeleted", "startDate", "endDate"], where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); @@ -1119,6 +1121,7 @@ export class ProfileEmployeeController extends Controller { degree: item.name ? Extension.ToThaiNumber(item.name) : "", place: item.place ? Extension.ToThaiNumber(item.place) : "", duration: item.duration ? Extension.ToThaiNumber(item.duration) : "", + date: Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`) })) : [ { @@ -1126,6 +1129,7 @@ export class ProfileEmployeeController extends Controller { degree: "-", place: "-", duration: "-", + date: "-" }, ]; @@ -1167,13 +1171,15 @@ export class ProfileEmployeeController extends Controller { date: item.isDate ? `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)) : ""}` : `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.startDate))) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.endDate))) : ""}`, - degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", + degree: `${item.degree ?? ""} ${item.field ?? ""}`.trim(), + level: item.educationLevel })) : [ { institute: "-", date: "-", degree: "-", + level: "-" }, ]; const salary_raw = await this.salaryRepo.find({ @@ -1513,6 +1519,7 @@ export class ProfileEmployeeController extends Controller { positionSalaryAmount: item.positionSalaryAmount ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) : "", + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1525,9 +1532,10 @@ export class ProfileEmployeeController extends Controller { posLevel: "-", amount: "-", positionSalaryAmount: "-", + refDoc: "-" }, ]; - + // ลูกจ้างยังไม่มีรักษาการและช่วยราชการ const actposition_raw = await this.profileActpositionRepo.find({ select: ["dateStart", "dateEnd", "position", "isDeleted"], where: { profileEmployeeId: id, isDeleted: false }, @@ -1599,30 +1607,36 @@ export class ProfileEmployeeController extends Controller { order: { createdAt: "ASC" }, }); const duty = - duty_raw.length > 0 - ? duty_raw.map((item) => ({ - date: - item.dateStart && item.dateEnd - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) - : item.dateStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) - : item.dateEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) - : "", - refCommandDate: item.refCommandDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) - : "", - refCommandNo: item.refCommandNo ? Extension.ToThaiNumber(item.refCommandNo) : "", - })) - : [ - { - date: "-", - refCommandDate: "-", - refCommandNo: "-", - }, - ]; + duty_raw.length > 0 + ? duty_raw.map((item) => ({ + date: + item.dateStart && item.dateEnd + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) + : item.dateStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) + : item.dateEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) + : "", + type: "-", + detail: Extension.ToThaiNumber(item.detail), + agency: "-", + refCommandNo: item.refCommandNo + ? item.refCommandDate + ? Extension.ToThaiNumber(`${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`) + : Extension.ToThaiNumber(item.refCommandNo) + : "-", + })) + : [ + { + date: "-", + type: "-", + detail: "-", + agency: "-", + refCommandNo: "-", + }, + ]; const assessments_raw = await this.profileAssessmentsRepository.find({ where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, @@ -1631,7 +1645,9 @@ export class ProfileEmployeeController extends Controller { assessments_raw.length > 0 ? assessments_raw.map((item) => ({ year: item.year ? Extension.ToThaiNumber((parseInt(item.year) + 543).toString()) : "", - period: item.period && item.period == "APR" ? "เมษายน" : "ตุลาคม", + period: item.period && item.period == "APR" + ? Extension.ToThaiNumber(`1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`) + : Extension.ToThaiNumber(`1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`), point1: item.point1 ? Extension.ToThaiNumber(item.point1.toString()) : "", point1Total: item.point1Total ? Extension.ToThaiNumber(item.point1Total.toString()) @@ -1640,18 +1656,20 @@ export class ProfileEmployeeController extends Controller { point2Total: item.point2Total ? Extension.ToThaiNumber(item.point2Total.toString()) : "", - pointSum: item.pointSum ? Extension.ToThaiNumber(item.pointSum.toString()) : "", + pointSum: item.pointSum + ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) + : "", pointSumTh: item.pointSum ? Extension.textPoint(item.pointSum) : "", level: item.pointSum < 60.0 - ? "ต้องปรับปรุง (คะแนนต่ำกว่าร้อยละ ๖๐.๐๐)" + ? "ต้องปรับปรุง" : item.pointSum <= 69.99 && item.pointSum >= 60.0 - ? "พอใช้ (คะแนนร้อยละ ๖๐.๐๐ - ๖๙.๙๙)" + ? "พอใช้" : item.pointSum <= 79.99 && item.pointSum >= 70.0 - ? "ดี (คะแนนร้อยละ ๗๐.๐๐ - ๗๙.๙๙)" + ? "ดี" : item.pointSum <= 89.99 && item.pointSum >= 80.0 - ? "ดีมาก (คะแนนร้อยละ ๘๐.๐๐ - ๘๙.๙๙)" - : "ดีเด่น (คะแนนร้อยละ ๙๐.๐๐ ขึ้นไป)", + ? "ดีมาก" + : "ดีเด่น", })) : [ { @@ -1706,6 +1724,7 @@ export class ProfileEmployeeController extends Controller { ? Extension.ToThaiNumber(item.positionCee) : null, amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1716,9 +1735,83 @@ export class ProfileEmployeeController extends Controller { position: "-", posLevel: "-", amount: "-", + refDoc: "-" }, ]; + // ประวัติพ้นจากราชการ + const retireCodes = ['C-PM-12','C-PM-13','C-PM-17','C-PM-18','C-PM-19','C-PM-20','C-PM-23','C-PM-43']; + + let retires = []; + let inRetirePeriod = false; + let currentRetire = null; + + for (let i = 0; i < position_raw.length; i++) { + const item = position_raw[i]; + + if (retireCodes.includes(item.commandCode)) { + // เริ่มพ้นจากราชการ + currentRetire = { + commandName: item.commandName ?? "-", + startDate: item.commandDateAffect + }; + inRetirePeriod = true; + } + else if (inRetirePeriod && currentRetire) { + // เจอคำสั่งถัดไปที่ไม่ใช่การพ้นจากราชการ = วันกลับเข้า + const endDate = item.commandDateAffect; + let daysCount = 0; + + if (currentRetire.startDate && endDate) { + const start = new Date(currentRetire.startDate); + const end = new Date(endDate); + daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + } + + const startDateStr = currentRetire.startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(currentRetire.startDate)) + : "-"; + const endDateStr = endDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) + : "-"; + + retires.push({ + date: `${startDateStr} - ${endDateStr}`, + detail: currentRetire.commandName, + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + }); + + inRetirePeriod = false; + currentRetire = null; + } + } + + // กรณีไม่มีข้อมูล + if (retires.length === 0) { + retires.push({ date: "-", detail: "-", day: "-" }); + } + + const org = + (_child4 == null ? "" : _child4 + " ") + + (_child3 == null ? "" : _child3 + " ") + + (_child2 == null ? "" : _child2 + " ") + + (_child1 == null ? "" : _child1 + " ") + + (_root == null ? "" : _root).trim() + const _position = profiles?.position != null ? + profiles?.posLevel != null + ? `${profiles.position}${profiles.posLevel.posLevelName}` + : profiles.position + : "" + const ocAssistance = await this.profileAssistanceRepository.findOne({ + select: ["agency", "profileEmployeeId", "commandName", "status", "isDeleted"], + where: { + profileEmployeeId: id, + commandName: "ให้ช่วยราชการ", + status: "PENDING", + isDeleted: false, }, + order: { createdAt: "ASC" }, + }); + const sum = profiles ? Extension.ToThaiNumber( ( @@ -1766,7 +1859,8 @@ export class ProfileEmployeeController extends Controller { prefix: profiles?.prefix != null ? profiles.prefix : "", firstName: profiles?.firstName != null ? profiles.firstName : "", lastName: profiles?.lastName != null ? profiles.lastName : "", - position: profiles?.position != null ? profiles.position : "", + position: _position, + posAssistance: ocAssistance ? "-" : _position, // ปัจจุบันช่วยราชการยังไม่มีเก็บฟิลด์ตำแหน่ง amount: profiles?.amount != null ? Extension.ToThaiNumber(profiles.amount.toLocaleString()) : "", positionSalaryAmount: @@ -1782,12 +1876,13 @@ export class ProfileEmployeeController extends Controller { ? Extension.ToThaiNumber(profiles.amountSpecial.toLocaleString()) : "", salarySum: sum, - ocFullPath: - (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root), + ocFullPath: org, + ocAssistance: ocAssistance?.agency ?? org, + root: _root == null ? "" : _root, + agency: (_child4 == null ? "" : _child4 + " ") + + (_child3 == null ? "" : _child3 + " ") + + (_child2 == null ? "" : _child2 + " ") + + (_child1 == null ? "" : _child1).trim(), birthDate: profiles?.birthDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(profiles.birthDate)) : "", @@ -1916,6 +2011,7 @@ export class ProfileEmployeeController extends Controller { assessments, profileAbility, otherIncome, + retires }; return new HttpSuccess({ diff --git a/src/services/CommandService.ts b/src/services/CommandService.ts index 295d9ce8..b002462c 100644 --- a/src/services/CommandService.ts +++ b/src/services/CommandService.ts @@ -1,6 +1,17 @@ import { AppDataSource } from "../database/data-source"; import { CommandRecive } from "../entities/CommandRecive"; import { Command } from "../entities/Command"; +import { OrgRoot } from "../entities/OrgRoot"; +import { Profile } from "../entities/Profile"; + +export interface PosNumCodeSitResult { + posNumCodeSit: string; + posNumCodeSitAbb: string; + commandTypeName: string; + commandNo: string; + commandYear: number; + commandExcecuteDate: Date; +} /** * เรียงลำดับผู้ได้รับคำสั่งใหม่หลังจากลบรายการ และอัพเดทสถานะคำสั่งถ้าไม่มีผู้ได้รับคำสั่งเหลือ @@ -44,3 +55,92 @@ export async function reOrderCommandRecivesAndDelete( } } + +/** + * ดึงข้อมูล posNumCodeSit และ posNumCodeSitAbb จาก commandId + * @param commandId ID ของคำสั่ง + * @param status สถานะของคำสั่ง (ไม่บังคับส่ง) + * @returns Promise ข้อมูลชื่อหน่วยงานและชื่อย่อ + */ +export async function getPosNumCodeSit( + commandId: string, + status?: string +): Promise { + const commandRepo = AppDataSource.getRepository(Command); + const orgRootRepo = AppDataSource.getRepository(OrgRoot); + const profileRepo = AppDataSource.getRepository(Profile); + + let posNumCodeSit:string = ""; + let posNumCodeSitAbb:string = ""; + let commandTypeName:string = ""; + let commandNo:string = ""; + let commandYear:number = 0; + let commandExcecuteDate:Date = new Date; + + + let _command: Command | null; + if (!status) { + _command = await commandRepo.findOne({ + where: { id: commandId }, + relations: { commandType: true } + }); + } + else { + _command = await commandRepo.findOne({ + where: { id: commandId, status: status }, + relations: { commandType: true } + }); + } + + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await orgRootRepo.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 profileRepo.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 ?? ""; + } + commandTypeName = _command?.commandType?.name ?? ""; + commandNo = _command?.commandNo ?? ""; + commandYear = _command?.commandYear; + commandExcecuteDate = _command?.commandExcecuteDate; + } + + return { + posNumCodeSit, + posNumCodeSitAbb, + commandTypeName, + commandNo, + commandYear, + commandExcecuteDate, + }; +} From 251e87d2b25b7bb45ccfe17e7d108d3bd6c8b96f Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 20 Mar 2026 13:26:39 +0700 Subject: [PATCH 152/178] =?UTF-8?q?fix=20=E0=B8=96=E0=B9=89=E0=B8=B2=20USE?= =?UTF-8?q?R=20=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=20STAFF=20=E0=B8=88?= =?UTF-8?q?=E0=B8=B6=E0=B8=87=E0=B8=88=E0=B8=B0=E0=B8=81=E0=B8=A3=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B8=AA=E0=B8=B4=E0=B8=97=E0=B8=98=E0=B8=B4=E0=B9=8C?= =?UTF-8?q?=20#2359?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index f21473c4..016b4768 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -198,20 +198,10 @@ export class CommandController extends Controller { child3: null, child4: null, }; - if (!request.user.role.includes("SUPER_ADMIN")) { + if (request.user.role.includes("STAFF")) { _data = await new permission().PermissionOrgList(request, "COMMAND"); } if (isDirector || _data.privilege == "OWNER") { - // let _data: any = { - // root: null, - // child1: null, - // child2: null, - // child3: null, - // child4: null, - // }; - // if (!request.user.role.includes("SUPER_ADMIN")) { - // _data = await new permission().PermissionOrgList(request, "COMMAND"); - // } const profiles = await this.profileRepository .createQueryBuilder("profile") .leftJoinAndSelect("profile.current_holders", "current_holders") From 2fdf5e58543ce41020288b3964e7b4e4bcd62e5c Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 20 Mar 2026 16:55:27 +0700 Subject: [PATCH 153/178] =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B9=84?= =?UTF-8?q?=E0=B8=82=20middleware=20log=20=E0=B9=83=E0=B8=AB=E0=B9=89?= =?UTF-8?q?=E0=B9=83=E0=B8=8A=E0=B9=89=E0=B8=88=E0=B8=B2=E0=B8=81=20token?= =?UTF-8?q?=20#223?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileController.ts | 216 +++++++++---------- src/controllers/ProfileEmployeeController.ts | 212 +++++++++--------- src/middlewares/logs.ts | 17 +- 3 files changed, 224 insertions(+), 221 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index e4685a53..3e0d6fe1 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1107,11 +1107,11 @@ export class ProfileController extends Controller { })) : [ { - certificateType: "-", - issuer: "-", - certificateNo: "-", - detail: "-", - issueToExpireDate: "-", + certificateType: "", + issuer: "", + certificateNo: "", + detail: "", + issueToExpireDate: "", }, ]; const training_raw = await this.trainingRepository.find({ @@ -1130,11 +1130,11 @@ export class ProfileController extends Controller { })) : [ { - institute: "-", - degree: "-", - place: "-", - duration: "-", - date: "-" + institute: "", + degree: "", + place: "", + duration: "", + date: "" }, ]; @@ -1155,10 +1155,10 @@ export class ProfileController extends Controller { })) : [ { - disciplineYear: "-", - disciplineDetail: "-", - refNo: "-", - level: "-", + disciplineYear: "", + disciplineDetail: "", + refNo: "", + level: "", }, ]; @@ -1181,10 +1181,10 @@ export class ProfileController extends Controller { })) : [ { - institute: "-", - date: "-", - degree: "-", - level: "-" + institute: "", + date: "", + degree: "", + level: "" }, ]; const salary_raw = await this.salaryRepo.find({ @@ -1251,19 +1251,19 @@ export class ProfileController extends Controller { })) : [ { - commandName: "-", - salaryDate: "-", - position: "-", - posNo: "-", - salary: "-", - special: "-", - rank: "-", - refAll: "-", - positionLevel: "-", - positionType: "-", - positionAmount: "-", - fullName: "-", - ocFullPath: "-", + commandName: "", + salaryDate: "", + position: "", + posNo: "", + salary: "", + special: "", + rank: "", + refAll: "", + positionLevel: "", + positionType: "", + positionAmount: "", + fullName: "", + ocFullPath: "", }, ]; @@ -1310,18 +1310,18 @@ export class ProfileController extends Controller { })) : [ { - receiveDate: "-", - insigniaName: "-", - insigniaShortName: "-", - insigniaTypeName: "-", - no: "-", - issue: "-", - volumeNo: "-", - volume: "-", - section: "-", - page: "-", - refCommandDate: "-", - note: "-" + receiveDate: "", + insigniaName: "", + insigniaShortName: "", + insigniaTypeName: "", + no: "", + issue: "", + volumeNo: "", + volume: "", + section: "", + page: "", + refCommandDate: "", + note: "" }, ]; @@ -1417,10 +1417,10 @@ export class ProfileController extends Controller { })) : [ { - date: "-", - type: "-", - leaveDays: "-", - reason: "-", + date: "", + type: "", + leaveDays: "", + reason: "", }, ]; const children_raw = await this.profileChildrenRepository.find({ @@ -1439,11 +1439,11 @@ export class ProfileController extends Controller { : [ { no: "", - childrenPrefix: "-", - childrenFirstName: "-", - childrenLastName: "-", - childrenFullName: "-", - childrenLive: "-", + childrenPrefix: "", + childrenFirstName: "", + childrenLastName: "", + childrenFullName: "", + childrenLive: "", }, ]; const changeName_raw = await this.changeNameRepository.find({ @@ -1463,11 +1463,11 @@ export class ProfileController extends Controller { })) : [ { - createdAt: "-", - status: "-", - prefix: "-", - firstName: "-", - lastName: "-", + createdAt: "", + status: "", + prefix: "", + firstName: "", + lastName: "", }, ]; @@ -1487,8 +1487,8 @@ export class ProfileController extends Controller { })) : [ { - birthDateOld: "-", - birthDate: "-", + birthDateOld: "", + birthDate: "", }, ]; @@ -1529,16 +1529,16 @@ export class ProfileController extends Controller { })) : [ { - commandName: "-", - commandDateAffect: "-", - commandDateSign: "-", - posNo: "-", - position: "-", - posType: "-", - posLevel: "-", - amount: "-", - positionSalaryAmount: "-", - refDoc: "-" + commandName: "", + commandDateAffect: "", + commandDateSign: "", + posNo: "", + position: "", + posType: "", + posLevel: "", + amount: "", + positionSalaryAmount: "", + refDoc: "" }, ]; @@ -1591,7 +1591,7 @@ export class ProfileController extends Controller { // กรณีไม่มีข้อมูล if (retires.length === 0) { - retires.push({ date: "-", detail: "-", day: "-" }); + retires.push({ date: "", detail: "", day: "" }); } // รักษาการ @@ -1674,12 +1674,12 @@ export class ProfileController extends Controller { } else { _actpositions.push({ - date: "-", - position: "-", - commandName: "-", - agency: "-", - note: "-", - document: "-", + date: "", + position: "", + commandName: "", + agency: "", + note: "", + document: "", }); } // ช่วยราชการ @@ -1732,12 +1732,12 @@ export class ProfileController extends Controller { } else { _assistances.push({ - date: "-", - position: "-", - commandName: "-", - agency: "-", - note: "-", - document: "-", + date: "", + position: "", + commandName: "", + agency: "", + note: "", + document: "", }); } // Merge รักษาการ และ ช่วยราชาร @@ -1771,11 +1771,11 @@ export class ProfileController extends Controller { })) : [ { - date: "-", - type: "-", - detail: "-", - agency: "-", - refCommandNo: "-", + date: "", + type: "", + detail: "", + agency: "", + refCommandNo: "", }, ]; const assessments_raw = await this.profileAssessmentsRepository.find({ @@ -1814,13 +1814,13 @@ export class ProfileController extends Controller { })) : [ { - year: "-", - period: "-", - point1: "-", - point2: "-", - pointSum: "-", - pointSumTh: "-", - level: "-", + year: "", + period: "", + point1: "", + point2: "", + pointSum: "", + pointSumTh: "", + level: "", }, ]; const profileAbility_raw = await this.profileAbilityRepo.find({ @@ -1835,8 +1835,8 @@ export class ProfileController extends Controller { })) : [ { - field: "-", - detail: "-", + field: "", + detail: "", }, ]; @@ -1870,14 +1870,14 @@ export class ProfileController extends Controller { })) : [ { - commandName: "-", - commandDateAffect: "-", - commandDateSign: "-", - commandNo: "-", - position: "-", - posLevel: "-", - amount: "-", - refDoc: "-" + commandName: "", + commandDateAffect: "", + commandDateSign: "", + commandNo: "", + position: "", + posLevel: "", + amount: "", + refDoc: "" }, ]; @@ -1924,15 +1924,15 @@ export class ProfileController extends Controller { ) : ""; - let portfolios: any; + let portfolios:any[] = []; await new CallAPI() .GetData(req, `/development/portfolio/kk1/${profiles?.keycloak}`) .then((x) => { - portfolios = x; + portfolios = Array.isArray(x) ? x : []; }) .catch(() => {}); - if (!portfolios) { - portfolios = [{ name: "-", year: "-", position: "-" }]; + if (portfolios.length == 0) { + portfolios = [{ name: "", year: "", position: "" }]; } else { portfolios = portfolios.map((x: any) => ({ name: x.name ? Extension.ToThaiNumber(x.name) : "-", diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index c87f61f9..559e5b70 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1022,12 +1022,12 @@ export class ProfileEmployeeController extends Controller { order: { lastUpdatedAt: "DESC" }, }); - const posMasterId = - profiles.current_holders == null || - profiles.current_holders.length == 0 || - profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null - ? null - : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.id; + // const posMasterId = + // profiles.current_holders == null || + // profiles.current_holders.length == 0 || + // profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + // ? null + // : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.id; const root = profiles.current_holders == null || @@ -1102,11 +1102,11 @@ export class ProfileEmployeeController extends Controller { })) : [ { - certificateType: "-", - issuer: "-", - certificateNo: "-", - detail: "-", - issueToExpireDate: "-", + certificateType: "", + issuer: "", + certificateNo: "", + detail: "", + issueToExpireDate: "", }, ]; const training_raw = await this.trainingRepository.find({ @@ -1125,11 +1125,11 @@ export class ProfileEmployeeController extends Controller { })) : [ { - institute: "-", - degree: "-", - place: "-", - duration: "-", - date: "-" + institute: "", + degree: "", + place: "", + duration: "", + date: "" }, ]; @@ -1150,10 +1150,10 @@ export class ProfileEmployeeController extends Controller { })) : [ { - disciplineYear: "-", - disciplineDetail: "-", - refNo: "-", - level: "-", + disciplineYear: "", + disciplineDetail: "", + refNo: "", + level: "", }, ]; @@ -1176,10 +1176,10 @@ export class ProfileEmployeeController extends Controller { })) : [ { - institute: "-", - date: "-", - degree: "-", - level: "-" + institute: "", + date: "", + degree: "", + level: "" }, ]; const salary_raw = await this.salaryRepo.find({ @@ -1246,19 +1246,19 @@ export class ProfileEmployeeController extends Controller { })) : [ { - commandName: "-", - salaryDate: "-", - position: "-", - posNo: "-", - salary: "-", - special: "-", - rank: "-", - refAll: "-", - positionLevel: "-", - positionType: "-", - positionAmount: "-", - fullName: "-", - ocFullPath: "-", + commandName: "", + salaryDate: "", + position: "", + posNo: "", + salary: "", + special: "", + rank: "", + refAll: "", + positionLevel: "", + positionType: "", + positionAmount: "", + fullName: "", + ocFullPath: "", }, ]; @@ -1305,17 +1305,17 @@ export class ProfileEmployeeController extends Controller { })) : [ { - receiveDate: "-", - insigniaName: "-", - insigniaShortName: "-", - insigniaTypeName: "-", - no: "-", - issue: "-", - volumeNo: "-", - volume: "-", - section: "-", - page: "-", - refCommandDate: "-", + receiveDate: "", + insigniaName: "", + insigniaShortName: "", + insigniaTypeName: "", + no: "", + issue: "", + volumeNo: "", + volume: "", + section: "", + page: "", + refCommandDate: "", }, ]; @@ -1411,10 +1411,10 @@ export class ProfileEmployeeController extends Controller { })) : [ { - date: "-", - type: "-", - leaveDays: "-", - reason: "-", + date: "", + type: "", + leaveDays: "", + reason: "", }, ]; const children_raw = await this.profileChildrenRepository.find({ @@ -1433,11 +1433,11 @@ export class ProfileEmployeeController extends Controller { : [ { no: "", - childrenPrefix: "-", - childrenFirstName: "-", - childrenLastName: "-", - childrenFullName: "-", - childrenLive: "-", + childrenPrefix: "", + childrenFirstName: "", + childrenLastName: "", + childrenFullName: "", + childrenLive: "", }, ]; const changeName_raw = await this.changeNameRepository.find({ @@ -1457,11 +1457,11 @@ export class ProfileEmployeeController extends Controller { })) : [ { - createdAt: "-", - status: "-", - prefix: "-", - firstName: "-", - lastName: "-", + createdAt: "", + status: "", + prefix: "", + firstName: "", + lastName: "", }, ]; @@ -1481,8 +1481,8 @@ export class ProfileEmployeeController extends Controller { })) : [ { - birthDateOld: "-", - birthDate: "-", + birthDateOld: "", + birthDate: "", }, ]; @@ -1523,16 +1523,16 @@ export class ProfileEmployeeController extends Controller { })) : [ { - commandName: "-", - commandDateAffect: "-", - commandDateSign: "-", - posNo: "-", - position: "-", - posType: "-", - posLevel: "-", - amount: "-", - positionSalaryAmount: "-", - refDoc: "-" + commandName: "", + commandDateAffect: "", + commandDateSign: "", + posNo: "", + position: "", + posType: "", + posLevel: "", + amount: "", + positionSalaryAmount: "", + refDoc: "" }, ]; // ลูกจ้างยังไม่มีรักษาการและช่วยราชการ @@ -1567,11 +1567,11 @@ export class ProfileEmployeeController extends Controller { })) : [ { - date: "-", - position: "-", - commandName: "-", - agency: "-", - document: "-", + date: "", + position: "", + commandName: "", + agency: "", + document: "", }, ]; const _assistance = @@ -1594,11 +1594,11 @@ export class ProfileEmployeeController extends Controller { })) : [ { - date: "-", - position: "-", - commandName: "-", - agency: "-", - document: "-", + date: "", + position: "", + commandName: "", + agency: "", + document: "", }, ]; const actposition = [..._actposition, ..._assistance]; @@ -1630,11 +1630,11 @@ export class ProfileEmployeeController extends Controller { })) : [ { - date: "-", - type: "-", - detail: "-", - agency: "-", - refCommandNo: "-", + date: "", + type: "", + detail: "", + agency: "", + refCommandNo: "", }, ]; const assessments_raw = await this.profileAssessmentsRepository.find({ @@ -1673,12 +1673,12 @@ export class ProfileEmployeeController extends Controller { })) : [ { - year: "-", - period: "-", - point1: "-", - point2: "-", - pointSum: "-", - pointSumTh: "-", + year: "", + period: "", + point1: "", + point2: "", + pointSum: "", + pointSumTh: "", }, ]; const profileAbility_raw = await this.profileAbilityRepo.find({ @@ -1693,8 +1693,8 @@ export class ProfileEmployeeController extends Controller { })) : [ { - field: "-", - detail: "-", + field: "", + detail: "", }, ]; @@ -1728,14 +1728,14 @@ export class ProfileEmployeeController extends Controller { })) : [ { - commandName: "-", - commandDateAffect: "-", - commandDateSign: "-", - commandNo: "-", - position: "-", - posLevel: "-", - amount: "-", - refDoc: "-" + commandName: "", + commandDateAffect: "", + commandDateSign: "", + commandNo: "", + position: "", + posLevel: "", + amount: "", + refDoc: "" }, ]; @@ -1788,7 +1788,7 @@ export class ProfileEmployeeController extends Controller { // กรณีไม่มีข้อมูล if (retires.length === 0) { - retires.push({ date: "-", detail: "-", day: "-" }); + retires.push({ date: "", detail: "", day: "" }); } const org = diff --git a/src/middlewares/logs.ts b/src/middlewares/logs.ts index 1bff2060..3f1a5963 100644 --- a/src/middlewares/logs.ts +++ b/src/middlewares/logs.ts @@ -67,14 +67,17 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4; - // Get profile from cache - const profileByKeycloak = await logMemoryStore.getProfileByKeycloak( - req.app.locals.logData.userId, - ); + // // Get profile from cache + // const profileByKeycloak = await logMemoryStore.getProfileByKeycloak( + // req.app.locals.logData.userId, + // ); - // Get rootId from cache - const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id); - // console.log("ancestorDNA:", rootId); + // // Get rootId from cache + // const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id); + // // console.log("ancestorDNA:", rootId); + + // Get rootId from token + const rootId = req.app.locals.logData?.orgRootDnaId; if (level === 1 && res.statusCode < 500) return; if (level === 2 && res.statusCode < 400) return; From 5a4b7c92a393b177637a5bc4f8cc170ec2c28948 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 20 Mar 2026 17:51:39 +0700 Subject: [PATCH 154/178] fix blank row report kk1 --- src/controllers/ProfileController.ts | 14 +- src/controllers/ProfileEmployeeController.ts | 142 ++++++++++--------- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 3e0d6fe1..ccb231a4 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1385,6 +1385,10 @@ export class ProfileController extends Controller { } }); + if (leaves.length === 0) { + leaves.push({year:""}); + } + const leave2_raw = await this.profileLeaveRepository .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") @@ -1730,16 +1734,6 @@ export class ProfileController extends Controller { }); } } - else { - _assistances.push({ - date: "", - position: "", - commandName: "", - agency: "", - note: "", - document: "", - }); - } // Merge รักษาการ และ ช่วยราชาร const actposition = [..._actpositions, ..._assistances]; diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 559e5b70..89293c5d 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1379,6 +1379,10 @@ export class ProfileEmployeeController extends Controller { } }); + if (leaves.length === 0) { + leaves.push({year:""}); + } + const leave2_raw = await this.profileLeaveRepository .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") @@ -1536,72 +1540,78 @@ export class ProfileEmployeeController extends Controller { }, ]; // ลูกจ้างยังไม่มีรักษาการและช่วยราชการ - const actposition_raw = await this.profileActpositionRepo.find({ - select: ["dateStart", "dateEnd", "position", "isDeleted"], - where: { profileEmployeeId: id, isDeleted: false }, - order: { createdAt: "ASC" }, - }); - const assistance_raw = await this.profileAssistanceRepository.find({ - select: ["dateStart", "dateEnd", "commandName", "agency", "document", "isDeleted"], - where: { profileEmployeeId: id, isDeleted: false }, - order: { createdAt: "ASC" }, - }); + // const actposition_raw = await this.profileActpositionRepo.find({ + // select: ["dateStart", "dateEnd", "position", "isDeleted"], + // where: { profileEmployeeId: id, isDeleted: false }, + // order: { createdAt: "ASC" }, + // }); + // const assistance_raw = await this.profileAssistanceRepository.find({ + // select: ["dateStart", "dateEnd", "commandName", "agency", "document", "isDeleted"], + // where: { profileEmployeeId: id, isDeleted: false }, + // order: { createdAt: "ASC" }, + // }); - const _actposition = - actposition_raw.length > 0 - ? actposition_raw.map((item) => ({ - date: - item.dateStart && item.dateEnd - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) - : item.dateStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) - : item.dateEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) - : "", - position: item.position ? Extension.ToThaiNumber(item.position) : "", - commandName: "รักษาการในตำแหน่ง", - agency: "", - document: "", - })) - : [ - { - date: "", - position: "", - commandName: "", - agency: "", - document: "", - }, - ]; - const _assistance = - assistance_raw.length > 0 - ? assistance_raw.map((item) => ({ - date: - item.dateStart && item.dateEnd - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) - : item.dateStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) - : item.dateEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) - : "", - position: "", - commandName: item.commandName ? Extension.ToThaiNumber(item.commandName) : "", - agency: item.agency ? Extension.ToThaiNumber(item.agency) : "", - document: item.document ? Extension.ToThaiNumber(item.document) : "", - })) - : [ - { - date: "", - position: "", - commandName: "", - agency: "", - document: "", - }, - ]; - const actposition = [..._actposition, ..._assistance]; + // const _actposition = + // actposition_raw.length > 0 + // ? actposition_raw.map((item) => ({ + // date: + // item.dateStart && item.dateEnd + // ? Extension.ToThaiNumber( + // `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + // ) + // : item.dateStart + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) + // : item.dateEnd + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) + // : "", + // position: item.position ? Extension.ToThaiNumber(item.position) : "", + // commandName: "รักษาการในตำแหน่ง", + // agency: "", + // document: "", + // })) + // : [ + // { + // date: "", + // position: "", + // commandName: "", + // agency: "", + // document: "", + // }, + // ]; + // const _assistance = + // assistance_raw.length > 0 + // ? assistance_raw.map((item) => ({ + // date: + // item.dateStart && item.dateEnd + // ? Extension.ToThaiNumber( + // `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + // ) + // : item.dateStart + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) + // : item.dateEnd + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) + // : "", + // position: "", + // commandName: item.commandName ? Extension.ToThaiNumber(item.commandName) : "", + // agency: item.agency ? Extension.ToThaiNumber(item.agency) : "", + // document: item.document ? Extension.ToThaiNumber(item.document) : "", + // })) + // : [ + // { + // date: "", + // position: "", + // commandName: "", + // agency: "", + // document: "", + // }, + // ]; + const actposition = [{ + date: "", + position: "", + commandName: "", + agency: "", + document: "", + }]; const duty_raw = await this.dutyRepository.find({ where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, @@ -1799,7 +1809,7 @@ export class ProfileEmployeeController extends Controller { (_root == null ? "" : _root).trim() const _position = profiles?.position != null ? profiles?.posLevel != null - ? `${profiles.position}${profiles.posLevel.posLevelName}` + ? Extension.ToThaiNumber(`${profiles.position}${profiles.posLevel.posLevelName}`) : profiles.position : "" const ocAssistance = await this.profileAssistanceRepository.findOne({ From 41ad2a44e809d920b7acfba34445e48f8db07fc5 Mon Sep 17 00:00:00 2001 From: Suphonchai Phoonsawat Date: Mon, 23 Mar 2026 10:35:24 +0700 Subject: [PATCH 155/178] Implement feature X to enhance user experience and optimize performance #1555 --- .../OrganizationDotnetController.ts | 945 +++++++++--------- 1 file changed, 464 insertions(+), 481 deletions(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 04d29f06..5a84f3bc 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -1,45 +1,44 @@ import { + Body, Controller, + Get, + Path, Post, Put, + Request, + Response, Route, Security, - Tags, - Body, - Path, - Request, SuccessResponse, - Response, - Get, + Tags, } from "tsoa"; -import { AppDataSource } from "../database/data-source"; -import HttpSuccess from "../interfaces/http-success"; -import HttpStatus from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { RequestWithUser } from "../middlewares/user"; -import { Profile } from "../entities/Profile"; import { And, Between, Brackets, In, IsNull, LessThanOrEqual, Not } from "typeorm"; -import { OrgRevision } from "../entities/OrgRevision"; -import { OrgRoot } from "../entities/OrgRoot"; +import { AppDataSource } from "../database/data-source"; +import { Assign } from "../entities/Assign"; +import { EmployeePosDict } from "../entities/EmployeePosDict"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { OrgChild1 } from "../entities/OrgChild1"; import { OrgChild2 } from "../entities/OrgChild2"; import { OrgChild3 } from "../entities/OrgChild3"; import { OrgChild4 } from "../entities/OrgChild4"; -import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { OrgRevision } from "../entities/OrgRevision"; +import { OrgRoot } from "../entities/OrgRoot"; import { Position } from "../entities/Position"; -import { Insignia } from "../entities/Insignia"; -import { CreateProfileInsignia, ProfileInsignia } from "../entities/ProfileInsignia"; import { PosMaster } from "../entities/PosMaster"; -import { EmployeePosMaster } from "../entities/EmployeePosMaster"; -import { EmployeePosDict } from "../entities/EmployeePosDict"; -import { calculateRetireLaw } from "../interfaces/utils"; -import Extension from "../interfaces/extension"; -import { PosMasterHistory } from "../entities/PosMasterHistory"; -import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; import { PosMasterAssign } from "../entities/PosMasterAssign"; -import { Assign } from "../entities/Assign"; -import { ProfileSalary } from "../entities/ProfileSalary"; +import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; +import { PosMasterHistory } from "../entities/PosMasterHistory"; +import { Profile } from "../entities/Profile"; import { ProfileEducation } from "../entities/ProfileEducation"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { CreateProfileInsignia, ProfileInsignia } from "../entities/ProfileInsignia"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import Extension from "../interfaces/extension"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import HttpSuccess from "../interfaces/http-success"; +import { calculateRetireLaw } from "../interfaces/utils"; +import { RequestWithUser } from "../middlewares/user"; @Route("api/v1/org/dotnet") @Tags("Dotnet") @Security("bearerAuth") @@ -60,12 +59,9 @@ export class OrganizationDotnetController extends Controller { private positionRepository = AppDataSource.getRepository(Position); private posMasterRepository = AppDataSource.getRepository(PosMaster); private posMasterHistoryRepository = AppDataSource.getRepository(PosMasterHistory); - private posMasterEmployeeHistoryRepository = - AppDataSource.getRepository(PosMasterEmployeeHistory); private empPosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); private insigniaRepo = AppDataSource.getRepository(ProfileInsignia); private employeePosDictRepository = AppDataSource.getRepository(EmployeePosDict); - private posMasterAssignRepo = AppDataSource.getRepository(PosMasterAssign); private assignRepository = AppDataSource.getRepository(Assign); private salaryRepo = AppDataSource.getRepository(ProfileSalary); private educationRepo = AppDataSource.getRepository(ProfileEducation); @@ -105,10 +101,40 @@ export class OrganizationDotnetController extends Controller { node?: number | null; page: number; pageSize: number; + selectedNodeId?: string | null; + selectedNode?: number | null; }, ) { let condition = "1=1"; let conditionParams = {}; + + let selectedNodeCondition = "1=1"; + let selectedNodeConditionParams = {}; + + if (body.selectedNodeId && body.selectedNode != null) { + switch (body.selectedNode) { + case 0: + selectedNodeCondition = "orgRoot.ancestorDNA = :selectedNodeId"; + break; + case 1: + selectedNodeCondition = "orgChild1.ancestorDNA = :selectedNodeId"; + break; + case 2: + selectedNodeCondition = "orgChild2.ancestorDNA = :selectedNodeId"; + break; + case 3: + selectedNodeCondition = "orgChild3.ancestorDNA = :selectedNodeId"; + break; + case 4: + selectedNodeCondition = "orgChild4.ancestorDNA = :selectedNodeId"; + break; + default: + selectedNodeCondition = "1=1"; + break; + } + selectedNodeConditionParams = { selectedNodeId: body.selectedNodeId }; + } + if (body.role === "CHILD") { switch (body.node) { case 0: @@ -214,6 +240,7 @@ export class OrganizationDotnetController extends Controller { }), ) .andWhere(condition, conditionParams) + .andWhere(selectedNodeCondition, selectedNodeConditionParams) .select([ "profile.id", "profile.citizenId", @@ -405,26 +432,24 @@ export class OrganizationDotnetController extends Controller { } /** - * API Get ProfileId - * - * @summary Get ProfileId - * - */ + * API Get ProfileId + * + * @summary Get ProfileId + * + */ @Get("get-profileId") - async getProfileInbox( - @Request() request: { user: Record }, - ) { - let profile: any + async getProfileInbox(@Request() request: { user: Record }) { + let profile: any; //OFF profile = await this.profileRepo.findOne({ where: { keycloak: request.user.sub }, - select: { id: true } + select: { id: true }, }); //EMP if (!profile) { profile = await this.profileEmpRepo.findOne({ where: { keycloak: request.user.sub }, - select: { id: true } + select: { id: true }, }); if (!profile) { if (request.user.role.includes("SUPER_ADMIN")) { @@ -435,8 +460,8 @@ export class OrganizationDotnetController extends Controller { } } const result = { - profileId: profile ? profile.id : null - } + profileId: profile ? profile.id : null, + }; return new HttpSuccess(result); } @@ -1045,10 +1070,10 @@ export class OrganizationDotnetController extends Controller { let positionLeaveName = profile.posType != null && - profile.posLevel != null && - (profile.posType.posTypeName == "บริหาร" || profile.posType.posTypeName == "อำนวยการ") + profile.posLevel != null && + (profile.posType.posTypeName == "บริหาร" || profile.posType.posTypeName == "อำนวยการ") ? `${profile.posType?.posTypeName ?? ""}${profile.posLevel?.posLevelName ?? ""}` - : profile.posLevel?.posLevelName ?? null; + : (profile.posLevel?.posLevelName ?? null); const _profileCurrent = profile?.current_holders?.find( (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, @@ -1236,9 +1261,10 @@ export class OrganizationDotnetController extends Controller { profileInsignia: profile.profileInsignias.length > 0 ? profile.profileInsignias[0] : null, profileType: "OFFICER", positionLeaveName: positionLeaveName, - posExecutiveName: position == null || position.posExecutive == null - ? null - : position.posExecutive.posExecutiveName, + posExecutiveName: + position == null || position.posExecutive == null + ? null + : position.posExecutive.posExecutiveName, oc: oc, }; @@ -1248,8 +1274,8 @@ export class OrganizationDotnetController extends Controller { @Get("keycloak/{keycloakId}") async GetProfileByKeycloakIdAsync(@Path() keycloakId: string) { /* ========================= - * 1. Load profile - * ========================= */ + * 1. Load profile + * ========================= */ const profile = await this.profileRepo.findOne({ where: { keycloak: keycloakId }, relations: { @@ -1297,7 +1323,7 @@ export class OrganizationDotnetController extends Controller { }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const currentHolder = profile.current_holders?.find( - x => + (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); @@ -1346,8 +1372,7 @@ export class OrganizationDotnetController extends Controller { .getOne(); if (pos?.current_holder) { - commanderFullname = - `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; + commanderFullname = `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; commanderPositionName = pos.current_holder.position; commanderId = pos.current_holder.id; commanderKeycloak = pos.current_holder.keycloak; @@ -1381,11 +1406,10 @@ export class OrganizationDotnetController extends Controller { const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : profile.posLevel?.posLevelName ?? null; + : (profile.posLevel?.posLevelName ?? null); const mapProfile = { id: profile.id, @@ -1471,12 +1495,11 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * 2. current holder - * ========================================= */ + * 2. current holder + * ========================================= */ const currentHolder = profile.current_holders?.find( - x => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, + (x) => + x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); const org = { @@ -1488,8 +1511,8 @@ export class OrganizationDotnetController extends Controller { }; /* ================================================= - * 3. หา commander - * ================================================= */ + * 3. หา commander + * ================================================= */ let commanderFullname = ""; let commanderPositionName = ""; let commanderId = ""; @@ -1526,16 +1549,15 @@ export class OrganizationDotnetController extends Controller { .getOne(); if (pos?.current_holder) { - commanderFullname = - `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; + commanderFullname = `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; commanderPositionName = pos.current_holder.position; commanderId = pos.current_holder.id; commanderKeycloak = pos.current_holder.keycloak; } /* ========================================= - * 4. salary / insignia (เอาแค่ล่าสุด) - * ========================================= */ + * 4. salary / insignia (เอาแค่ล่าสุด) + * ========================================= */ const [latestSalary, latestInsignia] = await Promise.all([ this.salaryRepo.findOne({ where: { profileId: profile.id }, @@ -1548,8 +1570,8 @@ export class OrganizationDotnetController extends Controller { ]); /* ========================================= - * 5. position executive - * ========================================= */ + * 5. position executive + * ========================================= */ const position = await this.positionRepository.findOne({ where: { positionIsSelected: true, @@ -1563,8 +1585,8 @@ export class OrganizationDotnetController extends Controller { }); /* ========================================= - * 6. OC name - * ========================================= */ + * 6. OC name + * ========================================= */ let oc = ""; if (currentHolder) { if (!currentHolder.orgChild1Id) { @@ -1581,19 +1603,18 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * 7. position level name - * ========================================= */ + * 7. position level name + * ========================================= */ const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : profile.posLevel?.posLevelName ?? null; + : (profile.posLevel?.posLevelName ?? null); /* ========================================= - * 8. map response - * ========================================= */ + * 8. map response + * ========================================= */ const mapProfile = { id: profile.id, avatar: profile.avatar, @@ -1681,8 +1702,8 @@ export class OrganizationDotnetController extends Controller { @Get("by-keycloak/{keycloakId}") async NewGetProfileByKeycloakIdAsync(@Path() keycloakId: string) { /* ========================= - * 1. Load profile - * ========================= */ + * 1. Load profile + * ========================= */ const profile = await this.profileRepo.findOne({ where: { keycloak: keycloakId }, relations: { @@ -1718,7 +1739,7 @@ export class OrganizationDotnetController extends Controller { }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const currentHolder = profile.current_holders?.find( - x => + (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); @@ -1772,9 +1793,10 @@ export class OrganizationDotnetController extends Controller { mouthSalaryAmount: profile.mouthSalaryAmount, posType: profile.posType?.posTypeName ?? null, - posLevel: profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null - ? null - : `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, + posLevel: + profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null + ? null + : `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, oc, root: currentHolder?.orgRoot?.orgRootName ?? null, @@ -1798,17 +1820,16 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * 2. current holder - * ========================================= */ + * 2. current holder + * ========================================= */ const currentHolder = profile.current_holders?.find( - x => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, + (x) => + x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); /* ========================================= - * 5. position executive - * ========================================= */ + * 5. position executive + * ========================================= */ const position = await this.positionRepository.findOne({ where: { positionIsSelected: true, @@ -1822,8 +1843,8 @@ export class OrganizationDotnetController extends Controller { }); /* ========================================= - * 6. OC name - * ========================================= */ + * 6. OC name + * ========================================= */ let oc = ""; if (currentHolder) { if (!currentHolder.orgChild1Id) { @@ -1840,19 +1861,18 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * 7. position level name - * ========================================= */ + * 7. position level name + * ========================================= */ const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : profile.posLevel?.posLevelName ?? null; + : (profile.posLevel?.posLevelName ?? null); /* ========================================= - * 8. map response - * ========================================= */ + * 8. map response + * ========================================= */ const mapProfile = { profileType: "OFFICER", id: profile.id, @@ -1916,8 +1936,8 @@ export class OrganizationDotnetController extends Controller { @Get("by-keycloak2/{keycloakId}") async NewGetProfileByKeycloak2IdAsync(@Path() keycloakId: string) { /* ========================= - * 1. Load profile - * ========================= */ + * 1. Load profile + * ========================= */ const profile = await this.profileRepo.findOne({ where: { keycloak: keycloakId }, relations: { @@ -1961,7 +1981,7 @@ export class OrganizationDotnetController extends Controller { }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const currentHolder = profile.current_holders?.find( - x => + (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); @@ -1997,8 +2017,7 @@ export class OrganizationDotnetController extends Controller { .getOne(); if (pos?.current_holder) { - commanderFullname = - `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; + commanderFullname = `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; commanderPositionName = pos.current_holder.position; commanderId = pos.current_holder.id; } @@ -2052,9 +2071,10 @@ export class OrganizationDotnetController extends Controller { mouthSalaryAmount: profile.mouthSalaryAmount, posType: profile.posType?.posTypeName ?? null, - posLevel: profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null - ? null - : `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, + posLevel: + profile.posType?.posTypeShortName == null && profile.posLevel?.posLevelName == null + ? null + : `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, oc, currentAddress: profile.currentAddress, @@ -2088,17 +2108,16 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * 2. current holder - * ========================================= */ + * 2. current holder + * ========================================= */ const currentHolder = profile.current_holders?.find( - x => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, + (x) => + x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); /* ================================================= - * 3. หา commander - * ================================================= */ + * 3. หา commander + * ================================================= */ const pos = await this.posMasterRepository .createQueryBuilder("pos") .leftJoinAndSelect("pos.current_holder", "holder") @@ -2129,16 +2148,15 @@ export class OrganizationDotnetController extends Controller { ) .getOne(); - if (pos?.current_holder) { - commanderFullname = - `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; + if (pos?.current_holder) { + commanderFullname = `${pos.current_holder.prefix}${pos.current_holder.firstName} ${pos.current_holder.lastName}`; commanderPositionName = pos.current_holder.position; commanderId = pos.current_holder.id; } /* ========================================= - * 5. position executive - * ========================================= */ + * 5. position executive + * ========================================= */ const position = await this.positionRepository.findOne({ where: { positionIsSelected: true, @@ -2152,8 +2170,8 @@ export class OrganizationDotnetController extends Controller { }); /* ========================================= - * 6. OC name - * ========================================= */ + * 6. OC name + * ========================================= */ let oc = ""; if (currentHolder) { if (!currentHolder.orgChild1Id) { @@ -2170,19 +2188,18 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * 7. position level name - * ========================================= */ + * 7. position level name + * ========================================= */ const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : profile.posLevel?.posLevelName ?? null; + : (profile.posLevel?.posLevelName ?? null); /* ========================================= - * 8. map response - * ========================================= */ + * 8. map response + * ========================================= */ const mapProfile = { profileType: "OFFICER", id: profile.id, @@ -2221,7 +2238,7 @@ export class OrganizationDotnetController extends Controller { posExecutiveName: position?.posExecutive?.posExecutiveName ?? null, positionLeaveName, oc, - + currentAddress: profile.currentAddress, currentSubDistrict: profile.currentSubDistrict?.name ?? null, currentDistrict: profile.currentDistrict?.name ?? null, @@ -2253,17 +2270,17 @@ export class OrganizationDotnetController extends Controller { } /** - * API Get Profile For Logs - * - * @summary API Get Profile For Logs - * - * @param {string} keycloakId keycloakId profile - */ + * API Get Profile For Logs + * + * @summary API Get Profile For Logs + * + * @param {string} keycloakId keycloakId profile + */ @Get("user-logs/{keycloakId}") async UserLogs(@Path() keycloakId: string) { /* ========================= - * 1. Load profile - * ========================= */ + * 1. Load profile + * ========================= */ const profile = await this.profileRepo.findOne({ where: { keycloak: keycloakId }, relations: { @@ -2287,7 +2304,7 @@ export class OrganizationDotnetController extends Controller { }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const currentHolder = profile.current_holders?.find( - x => + (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); @@ -2307,17 +2324,16 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * 2. current holder - * ========================================= */ + * 2. current holder + * ========================================= */ const currentHolder = profile.current_holders?.find( - x => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, + (x) => + x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); /* ========================================= - * 8. map response - * ========================================= */ + * 8. map response + * ========================================= */ const mapProfile = { profileId: profile.id, keycloak: profile.keycloak, @@ -3749,26 +3765,26 @@ export class OrganizationDotnetController extends Controller { profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; @@ -4059,26 +4075,26 @@ export class OrganizationDotnetController extends Controller { profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; return { @@ -4180,7 +4196,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("find/employee/position") async GetProfileByPositionEmpAsync( - @Request() req: RequestWithUser, @Body() body: { empPosId: string[]; @@ -4280,26 +4295,26 @@ export class OrganizationDotnetController extends Controller { profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; return { @@ -4547,7 +4562,7 @@ export class OrganizationDotnetController extends Controller { const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; @@ -4601,8 +4616,8 @@ export class OrganizationDotnetController extends Controller { orgChild2: true, orgChild3: true, orgChild4: true, - } - } + }, + }, }); if (!profile) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); @@ -4619,41 +4634,41 @@ export class OrganizationDotnetController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -4879,9 +4894,7 @@ export class OrganizationDotnetController extends Controller { // }), // ); const profile_ = profile.map((item: Profile) => { - const holder = item.current_holders?.find( - (x) => x.orgRevisionId === findRevision?.id, - ); + const holder = item.current_holders?.find((x) => x.orgRevisionId === findRevision?.id); const rootName = holder?.orgRoot?.orgRootName ?? null; @@ -5003,30 +5016,30 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot - ?.orgRootName; + ?.orgRootName; const shortName = item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; @@ -5101,7 +5114,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("keycloak-all-officer") async PostProfileWithKeycloakAllOfficer( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -5210,25 +5222,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName}${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -5272,7 +5284,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("keycloak-all-officer/date") async PostProfileWithKeycloakAllOfficerDate( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -5389,25 +5400,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName}${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -5451,7 +5462,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("none-validate-keycloak-all-officer") async PostProfileWithNoneValidateKeycloakAllOfficer( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -5559,25 +5569,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -5621,7 +5631,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("find-node-name") async findNodeName( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -5730,7 +5739,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("officer-by-admin-role") async GetOfficersByAdminRole( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -5785,8 +5793,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } - else if (body.role === "BROTHER") { + } else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -5827,7 +5834,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } + } // else if (body.role === "PARENT") { // typeCondition = { // orgRoot: { @@ -5992,25 +5999,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -6069,7 +6076,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("keycloak-all-employee") async PostProfileWithKeycloakAllEmployee( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -6158,25 +6164,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -6223,7 +6229,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("none-validate-keycloak-all-employee") async PostProfileWithNoneValidateKeycloakAllEmployee( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -6312,25 +6317,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -6377,7 +6382,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("employee-by-admin-role") async GetEmployeesByAdminRole( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -6434,8 +6438,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } - else if (body.role === "BROTHER") { + } else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -6476,7 +6479,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } + } // else if (body.role === "PARENT") { // typeCondition = { // orgRoot: { @@ -6641,25 +6644,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -7164,16 +7167,16 @@ export class OrganizationDotnetController extends Controller { currentAddress: profile && profile.currentAddress ? profile.currentAddress + - (profile.currentSubDistrict && profile.currentSubDistrict.name - ? " ตำบล/แขวง " + profile.currentSubDistrict.name - : "") + - (profile.currentDistrict && profile.currentDistrict.name - ? " อำเภอ/เขต " + profile.currentDistrict.name - : "") + - (profile.currentProvince && profile.currentProvince.name - ? " จังหวัด " + profile.currentProvince.name - : "") + - profile.currentZipCode + (profile.currentSubDistrict && profile.currentSubDistrict.name + ? " ตำบล/แขวง " + profile.currentSubDistrict.name + : "") + + (profile.currentDistrict && profile.currentDistrict.name + ? " อำเภอ/เขต " + profile.currentDistrict.name + : "") + + (profile.currentProvince && profile.currentProvince.name + ? " จังหวัด " + profile.currentProvince.name + : "") + + profile.currentZipCode : "-", oc: oc ?? "-", root: @@ -7209,26 +7212,26 @@ export class OrganizationDotnetController extends Controller { positions: _position && _position.length > 0 ? _position.slice(0, -1).map((x: any, idx: number) => ({ - positionName: x.positionName, - dateStart: x.commandDateAffect ?? null, - dateEnd: _position[idx + 1]?.commandDateAffect ?? null, - positionType: x.positionType, - positionLevel: x.positionLevel, - orgRoot: x.orgRoot, - orgChild1: x.orgChild1, - orgChild2: x.orgChild2, - orgChild3: x.orgChild3, - orgChild4: x.orgChild4, - })) + positionName: x.positionName, + dateStart: x.commandDateAffect ?? null, + dateEnd: _position[idx + 1]?.commandDateAffect ?? null, + positionType: x.positionType, + positionLevel: x.positionLevel, + orgRoot: x.orgRoot, + orgChild1: x.orgChild1, + orgChild2: x.orgChild2, + orgChild3: x.orgChild3, + orgChild4: x.orgChild4, + })) : [], educations: profile.profileEducations && profile.profileEducations.length > 0 ? profile.profileEducations.map((x) => ({ - educationLevel: x.educationLevel, - institute: x.institute ?? "-", - country: x.country ?? "-", - finishDate: x.finishDate, - })) + educationLevel: x.educationLevel, + institute: x.institute ?? "-", + country: x.country ?? "-", + finishDate: x.finishDate, + })) : [], }; return new HttpSuccess(mapEmpProfile); @@ -7278,16 +7281,16 @@ export class OrganizationDotnetController extends Controller { currentAddress: profile && profile.currentAddress ? profile.currentAddress + - (profile.currentSubDistrict && profile.currentSubDistrict.name - ? " ตำบล/แขวง " + profile.currentSubDistrict.name - : "") + - (profile.currentDistrict && profile.currentDistrict.name - ? " อำเภอ/เขต " + profile.currentDistrict.name - : "") + - (profile.currentProvince && profile.currentProvince.name - ? " จังหวัด " + profile.currentProvince.name - : "") + - profile.currentZipCode + (profile.currentSubDistrict && profile.currentSubDistrict.name + ? " ตำบล/แขวง " + profile.currentSubDistrict.name + : "") + + (profile.currentDistrict && profile.currentDistrict.name + ? " อำเภอ/เขต " + profile.currentDistrict.name + : "") + + (profile.currentProvince && profile.currentProvince.name + ? " จังหวัด " + profile.currentProvince.name + : "") + + profile.currentZipCode : "-", oc: oc ?? "-", root: @@ -7323,26 +7326,26 @@ export class OrganizationDotnetController extends Controller { positions: _position && _position.length > 0 ? _position.slice(0, -1).map((x: any, idx: number) => ({ - positionName: x.positionName, - dateStart: x.commandDateAffect ?? null, - dateEnd: _position[idx + 1]?.commandDateAffect ?? null, - positionType: x.positionType, - positionLevel: x.positionLevel, - orgRoot: x.orgRoot, - orgChild1: x.orgChild1, - orgChild2: x.orgChild2, - orgChild3: x.orgChild3, - orgChild4: x.orgChild4, - })) + positionName: x.positionName, + dateStart: x.commandDateAffect ?? null, + dateEnd: _position[idx + 1]?.commandDateAffect ?? null, + positionType: x.positionType, + positionLevel: x.positionLevel, + orgRoot: x.orgRoot, + orgChild1: x.orgChild1, + orgChild2: x.orgChild2, + orgChild3: x.orgChild3, + orgChild4: x.orgChild4, + })) : [], educations: profile.profileEducations && profile.profileEducations.length > 0 ? profile.profileEducations.map((x) => ({ - educationLevel: x.educationLevel, - institute: x.institute ?? "-", - country: x.country ?? "-", - finishDate: x.finishDate, - })) + educationLevel: x.educationLevel, + institute: x.institute ?? "-", + country: x.country ?? "-", + finishDate: x.finishDate, + })) : [], }; return new HttpSuccess(mapProfile); @@ -7350,10 +7353,7 @@ export class OrganizationDotnetController extends Controller { @Post("profile-leave/keycloak") async GetProfileLeaveReportByKeycloakIdAsync( - @Body() body: { - keycloakId: string, - report?: string - } + @Body() body: { keycloakId: string; report?: string }, ) { const profile = await this.profileRepo.findOne({ relations: { @@ -7396,14 +7396,14 @@ export class OrganizationDotnetController extends Controller { }, where: { keycloak: body.keycloakId, - } + }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); /* ========================================= - * current holder - * ========================================= */ + * current holder + * ========================================= */ const currentHolder = profile.current_holders?.find( - x => + (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); @@ -7425,37 +7425,40 @@ export class OrganizationDotnetController extends Controller { let _positions: any[] = []; let _educations: any[] = []; if (body.report && ["LEAVE16", "LEAVE18"].includes(body.report.trim().toUpperCase())) { - const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); let _currentDate = CURRENT_DATE[0].today; if (profile && profile?.isLeave) { _currentDate = - profile && profile.leaveDate ? Extension.toDateOnlyString(profile.leaveDate) : _currentDate; + profile && profile.leaveDate + ? Extension.toDateOnlyString(profile.leaveDate) + : _currentDate; } const positions = await AppDataSource.query("CALL GetProfileEmployeeSalaryPosition(?, ?)", [ profile!.id, _currentDate, ]); - _positions = positions[0].length > 0 - ? _positions.slice(0, -1).map((x: any, idx: number) => ({ - positionName: x.positionName, - dateStart: x.commandDateAffect ?? null, - dateEnd: _positions[idx + 1]?.commandDateAffect ?? null, - })) - : []; + _positions = + positions[0].length > 0 + ? _positions.slice(0, -1).map((x: any, idx: number) => ({ + positionName: x.positionName, + dateStart: x.commandDateAffect ?? null, + dateEnd: _positions[idx + 1]?.commandDateAffect ?? null, + })) + : []; const profileEducations = await this.educationRepo.find({ where: { profileEmployeeId: profile!.id, isDeleted: false }, order: { level: "ASC" }, }); - _educations = profileEducations.length > 0 - ? profileEducations.map((x) => ({ - educationLevel: x.educationLevel, - institute: x.institute ?? "-", - country: x.country ?? "-", - finishDate: x.finishDate, - })) - : []; + _educations = + profileEducations.length > 0 + ? profileEducations.map((x) => ({ + educationLevel: x.educationLevel, + institute: x.institute ?? "-", + country: x.country ?? "-", + finishDate: x.finishDate, + })) + : []; } const mapProfile = { @@ -7465,15 +7468,11 @@ export class OrganizationDotnetController extends Controller { lastName: profile.lastName, citizenId: profile.citizenId, birthDate: profile.birthDate, - retireDate: profile.birthDate - ? calculateRetireLaw(profile.birthDate) - : null, + retireDate: profile.birthDate ? calculateRetireLaw(profile.birthDate) : null, govAge: profile.dateAppoint ? `${Extension.CalculateGovAge(profile.dateAppoint, 0, 0)} ปี` : null, - age: profile.birthDate - ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") - : null, + age: profile.birthDate ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") : null, dateAppoint: profile.dateAppoint, dateCurrent: new Date(), amount: profile.amount, @@ -7481,16 +7480,16 @@ export class OrganizationDotnetController extends Controller { currentAddress: profile && profile.currentAddress ? profile.currentAddress + - (profile.currentSubDistrict && profile.currentSubDistrict.name - ? " ตำบล/แขวง " + profile.currentSubDistrict.name - : "") + - (profile.currentDistrict && profile.currentDistrict.name - ? " อำเภอ/เขต " + profile.currentDistrict.name - : "") + - (profile.currentProvince && profile.currentProvince.name - ? " จังหวัด " + profile.currentProvince.name + " " - : "") + - profile.currentZipCode + (profile.currentSubDistrict && profile.currentSubDistrict.name + ? " ตำบล/แขวง " + profile.currentSubDistrict.name + : "") + + (profile.currentDistrict && profile.currentDistrict.name + ? " อำเภอ/เขต " + profile.currentDistrict.name + : "") + + (profile.currentProvince && profile.currentProvince.name + ? " จังหวัด " + profile.currentProvince.name + " " + : "") + + profile.currentZipCode : "-", position: profile.position, posType: profile.posType?.posTypeName, @@ -7513,12 +7512,11 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * current holder - * ========================================= */ + * current holder + * ========================================= */ const currentHolder = profile.current_holders?.find( - x => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, + (x) => + x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, ); let oc = ""; @@ -7537,19 +7535,18 @@ export class OrganizationDotnetController extends Controller { } /* ========================================= - * posType + posLevel - * ========================================= */ + * posType + posLevel + * ========================================= */ const positionLeaveName = profile.posType && - profile.posLevel && - (profile.posType.posTypeName === "บริหาร" || - profile.posType.posTypeName === "อำนวยการ") + profile.posLevel && + (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : profile.posLevel?.posLevelName ?? null; + : (profile.posLevel?.posLevelName ?? null); /* ========================================= - * position executive - * ========================================= */ + * position executive + * ========================================= */ const _posExec = await this.positionRepository.findOne({ where: { positionIsSelected: true, @@ -7565,37 +7562,40 @@ export class OrganizationDotnetController extends Controller { let _positions: any[] = []; let _educations: any[] = []; if (body.report && ["LEAVE16", "LEAVE18"].includes(body.report.trim().toUpperCase())) { - const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); let _currentDate = CURRENT_DATE[0].today; if (profile && profile?.isLeave) { _currentDate = - profile && profile.leaveDate ? Extension.toDateOnlyString(profile.leaveDate) : _currentDate; + profile && profile.leaveDate + ? Extension.toDateOnlyString(profile.leaveDate) + : _currentDate; } const positions = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [ profile!.id, _currentDate, ]); - _positions = positions[0].length > 0 - ? _positions.slice(0, -1).map((x: any, idx: number) => ({ - positionName: x.positionName, - dateStart: x.commandDateAffect ?? null, - dateEnd: _positions[idx + 1]?.commandDateAffect ?? null, - })) - : []; + _positions = + positions[0].length > 0 + ? _positions.slice(0, -1).map((x: any, idx: number) => ({ + positionName: x.positionName, + dateStart: x.commandDateAffect ?? null, + dateEnd: _positions[idx + 1]?.commandDateAffect ?? null, + })) + : []; const profileEducations = await this.educationRepo.find({ where: { profileId: profile!.id, isDeleted: false }, order: { level: "ASC" }, }); - _educations = profileEducations.length > 0 - ? profileEducations.map((x) => ({ - educationLevel: x.educationLevel, - institute: x.institute ?? "-", - country: x.country ?? "-", - finishDate: x.finishDate, - })) - : []; + _educations = + profileEducations.length > 0 + ? profileEducations.map((x) => ({ + educationLevel: x.educationLevel, + institute: x.institute ?? "-", + country: x.country ?? "-", + finishDate: x.finishDate, + })) + : []; } const mapProfile = { @@ -7605,15 +7605,11 @@ export class OrganizationDotnetController extends Controller { lastName: profile.lastName, citizenId: profile.citizenId, birthDate: profile.birthDate, - retireDate: profile.birthDate - ? calculateRetireLaw(profile.birthDate) - : null, + retireDate: profile.birthDate ? calculateRetireLaw(profile.birthDate) : null, govAge: profile.dateAppoint ? `${Extension.CalculateGovAge(profile.dateAppoint, 0, 0)} ปี` : null, - age: profile.birthDate - ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") - : null, + age: profile.birthDate ? Extension.CalculateAgeStrV2(profile.birthDate, 0, 0, "GET") : null, dateAppoint: profile.dateAppoint, dateCurrent: new Date(), amount: profile.amount, @@ -7621,16 +7617,16 @@ export class OrganizationDotnetController extends Controller { currentAddress: profile && profile.currentAddress ? profile.currentAddress + - (profile.currentSubDistrict && profile.currentSubDistrict.name - ? " ตำบล/แขวง " + profile.currentSubDistrict.name - : "") + - (profile.currentDistrict && profile.currentDistrict.name - ? " อำเภอ/เขต " + profile.currentDistrict.name - : "") + - (profile.currentProvince && profile.currentProvince.name - ? " จังหวัด " + profile.currentProvince.name + " " - : "") + - profile.currentZipCode + (profile.currentSubDistrict && profile.currentSubDistrict.name + ? " ตำบล/แขวง " + profile.currentSubDistrict.name + : "") + + (profile.currentDistrict && profile.currentDistrict.name + ? " อำเภอ/เขต " + profile.currentDistrict.name + : "") + + (profile.currentProvince && profile.currentProvince.name + ? " จังหวัด " + profile.currentProvince.name + " " + : "") + + profile.currentZipCode : "-", position: profile.position ?? "-", posLevel: profile.posLevel?.posLevelName ?? "-", @@ -7661,7 +7657,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("find/insignia-requests-profile/{type}") async GetInsigniaRequestsProfileAsync( - @Request() req: RequestWithUser, @Path() type: string, @Body() body: { @@ -7791,7 +7786,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("officer-by-admin-rolev2") async GetOfficersByAdminRoleV2( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -7836,8 +7830,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } - else if (body.role === "BROTHER") { + } else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -7868,7 +7861,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } + } // else if (body.role === "PARENT") { // typeCondition = { // rootDnaId: body.nodeId, @@ -8016,7 +8009,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("officer-by-admin-rolev3") async GetOfficersByAdminRoleV3( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -8073,8 +8065,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } - else if (body.role === "BROTHER") { + } else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -8115,7 +8106,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } + } // else if (body.role === "PARENT") { // typeCondition = { // orgRoot: { @@ -8287,25 +8278,25 @@ export class OrganizationDotnetController extends Controller { item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != - null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild2 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgChild1 != null + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` : null; const Oc = @@ -8364,7 +8355,6 @@ export class OrganizationDotnetController extends Controller { */ @Post("officer-by-admin-rolev4") async GetOfficersByAdminRoleV4( - @Request() req: RequestWithUser, @Body() body: { node: number; @@ -8409,8 +8399,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } - else if (body.role === "BROTHER") { + } else if (body.role === "BROTHER") { switch (body.node) { case 0: typeCondition = { @@ -8441,7 +8430,7 @@ export class OrganizationDotnetController extends Controller { typeCondition = {}; break; } - } + } // else if (body.role === "PARENT") { // typeCondition = { // rootDnaId: body.nodeId, @@ -8608,17 +8597,16 @@ export class OrganizationDotnetController extends Controller { ); return new HttpSuccess( - (profile_ ?? []).sort((a, b) => - a.posNo.localeCompare(b.posNo, undefined, { numeric: true })) + (profile_ ?? []).sort((a, b) => a.posNo.localeCompare(b.posNo, undefined, { numeric: true })), ); } /** - * API ค้นหา กจ. - * - * @summary API ค้นหา กจ. - * - */ + * API ค้นหา กจ. + * + * @summary API ค้นหา กจ. + * + */ @Post("find-staff") async findHigher( @Body() @@ -8626,18 +8614,13 @@ export class OrganizationDotnetController extends Controller { profileId: string; assignId: string; }, - @Request() request: RequestWithUser, ) { const profile = await this.profileRepo.findOne({ where: { id: requestBody.profileId }, - relations: [ - "current_holders", - "current_holders.orgRevision" - ], + relations: ["current_holders", "current_holders.orgRevision"], }); - if (!profile) - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); + if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); const assign = await this.assignRepository.findOne({ where: { id: requestBody.assignId.trim().toLocaleUpperCase() }, @@ -8647,7 +8630,7 @@ export class OrganizationDotnetController extends Controller { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลระบบสิทธิ์หน้าที่ความรับผิดชอบ"); const currentHolder = profile.current_holders?.find( - h => h.orgRevision?.orgRevisionIsCurrent && !h.orgRevision?.orgRevisionIsDraft, + (h) => h.orgRevision?.orgRevisionIsCurrent && !h.orgRevision?.orgRevisionIsDraft, ); if (!currentHolder) @@ -8690,7 +8673,7 @@ export class OrganizationDotnetController extends Controller { profileId: profile.id, }) .andWhere("assign.assignId = :assignId", { - assignId: assign.id + assignId: assign.id, }) .getRawMany(); From a76bda34b452de4993b30d09a3e1980c5220a2ed Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 23 Mar 2026 15:36:54 +0700 Subject: [PATCH 156/178] =?UTF-8?q?Migration=20=E0=B8=9F=E0=B8=B4=E0=B8=A5?= =?UTF-8?q?=E0=B8=94=E0=B9=8C=E0=B8=95=E0=B8=B2=E0=B8=A3=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=20profileLeave,=20profileLeaveHistory=20&=20=E0=B8=AA=E0=B8=A3?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B8=95=E0=B8=B2=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=87=20profileAbsentLate,=20profileEmployeeAbsentLate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProfileAbsentLateController.ts | 217 ++++++++++++++++++ .../ProfileEmployeeAbsentLateController.ts | 217 ++++++++++++++++++ src/entities/Profile.ts | 4 + src/entities/ProfileAbsentLate.ts | 98 ++++++++ src/entities/ProfileEmployee.ts | 4 + src/entities/ProfileEmployeeAbsentLate.ts | 87 +++++++ src/entities/ProfileLeave.ts | 24 ++ ...5696453-create_table_profileAbsentLate_.ts | 34 +++ ...53766170-update_table_profileAbsentLate.ts | 21 ++ 9 files changed, 706 insertions(+) create mode 100644 src/controllers/ProfileAbsentLateController.ts create mode 100644 src/controllers/ProfileEmployeeAbsentLateController.ts create mode 100644 src/entities/ProfileAbsentLate.ts create mode 100644 src/entities/ProfileEmployeeAbsentLate.ts create mode 100644 src/migration/1774245696453-create_table_profileAbsentLate_.ts create mode 100644 src/migration/1774253766170-update_table_profileAbsentLate.ts diff --git a/src/controllers/ProfileAbsentLateController.ts b/src/controllers/ProfileAbsentLateController.ts new file mode 100644 index 00000000..b9e64a8f --- /dev/null +++ b/src/controllers/ProfileAbsentLateController.ts @@ -0,0 +1,217 @@ +import { + Body, + Controller, + Get, + Patch, + Path, + Post, + Request, + Route, + Security, + Tags, +} from "tsoa"; +import { AppDataSource } from "../database/data-source"; +import { In } from "typeorm"; +import { + ProfileAbsentLate, + CreateProfileAbsentLate, + CreateProfileAbsentLateBatch, + UpdateProfileAbsentLate, +} from "../entities/ProfileAbsentLate"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; +import { Profile } from "../entities/Profile"; +import permission from "../interfaces/permission"; +import { setLogDataDiff } from "../interfaces/utils"; + +@Route("api/v1/org/profile/absent-late") +@Tags("ProfileAbsentLate") +@Security("bearerAuth") +export class ProfileAbsentLateController extends Controller { + private profileRepo = AppDataSource.getRepository(Profile); + private absentLateRepo = AppDataSource.getRepository(ProfileAbsentLate); + + /** + * API ดึงข้อมูลการมาสาย/ขาดราชการของ user + * @summary API ดึงข้อมูลการมาสาย/ขาดราชการของ user + */ + @Get("user") + public async getAbsentLateUser(@Request() request: { user: Record }) { + const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + const record = await this.absentLateRepo.find({ + where: { profileId: profile.id, isDeleted: false }, + order: { stampDate: "DESC" }, + }); + return new HttpSuccess(record); + } + + /** + * API ดึงข้อมูลการมาสาย/ขาดราชการตาม profileId + * @summary API ดึงข้อมูลการมาสาย/ขาดราชการตาม profileId + * @param profileId คีย์ profile + */ + @Get("{profileId}") + public async getAbsentLate(@Path() profileId: string, @Request() req: RequestWithUser) { + let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_OFFICER"); + if (_workflow == false) + await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); + const record = await this.absentLateRepo.find({ + where: { profileId, isDeleted: false }, + order: { stampDate: "DESC" }, + }); + return new HttpSuccess(record); + } + + /** + * API สร้างข้อมูลการมาสาย/ขาดราชการ + * @summary API สร้างข้อมูลการมาสาย/ขาดราชการ + */ + @Post() + public async newAbsentLate( + @Request() req: RequestWithUser, + @Body() body: CreateProfileAbsentLate, + ) { + if (!body.profileId) { + throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileId"); + } + + const profile = await this.profileRepo.findOneBy({ id: body.profileId }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_OFFICER", profile.id); + + const before = null; + const data = new ProfileAbsentLate(); + + const meta = { + 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, { ...body, ...meta }); + await this.absentLateRepo.save(data, { data: req }); + setLogDataDiff(req, { before, after: data }); + + return new HttpSuccess(data.id); + } + + /** + * API สร้างข้อมูลการมาสาย/ขาดราชการ (สำหรับ Job) + * @summary API สร้างข้อมูลการมาสาย/ขาดราชการ (สำหรับ Job) + */ + @Post("batch") + public async newAbsentLateBatch( + @Request() req: RequestWithUser, + @Body() body: CreateProfileAbsentLateBatch, + ) { + if (!body.records || body.records.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณาระบุข้อมูลอย่างน้อย 1 รายการ"); + } + + const profileIds = [...new Set(body.records.map((r) => r.profileId))]; + const profiles = await this.profileRepo.findBy({ + id: In(profileIds), + }); + + const foundProfileIds = new Set(profiles.map((p) => p.id)); + const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileId)); + + if (validRecords.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ที่ระบุ"); + } + + const meta = { + createdUserId: req.user.sub, + createdFullName: req.user.name, + lastUpdateUserId: req.user.sub, + lastUpdateFullName: req.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + const records = validRecords.map((item) => { + const data = new ProfileAbsentLate(); + Object.assign(data, { ...item, ...meta }); + return data; + }); + + const result = await this.absentLateRepo.save(records, { data: req }); + + return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) }); + } + + /** + * API แก้ไขข้อมูลการมาสาย/ขาดราชการ + * @summary API แก้ไขข้อมูลการมาสาย/ขาดราชการ + * @param absentLateId คีย์การมาสาย/ขาดราชการ + */ + @Patch("{absentLateId}") + public async editAbsentLate( + @Request() req: RequestWithUser, + @Body() body: UpdateProfileAbsentLate, + @Path() absentLateId: string, + ) { + const record = await this.absentLateRepo.findOneBy({ id: absentLateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + await new permission().PermissionOrgUserUpdate( + req, + "SYS_REGISTRY_OFFICER", + record.profileId, + ); + + const before = structuredClone(record); + + Object.assign(record, body); + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = new Date(); + + await Promise.all([ + this.absentLateRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + ]); + + return new HttpSuccess(); + } + + /** + * API ลบข้อมูลการมาสาย/ขาดราชการ (Soft Delete) + * @summary API ลบข้อมูลการมาสาย/ขาดราชการ (Soft Delete) + * @param absentLateId คีย์การมาสาย/ขาดราชการ + */ + @Patch("update-delete/{absentLateId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() absentLateId: string, + ) { + const record = await this.absentLateRepo.findOneBy({ id: absentLateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); + + const before = structuredClone(record); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = new Date(); + + await Promise.all([ + this.absentLateRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + ]); + + return new HttpSuccess(); + } +} diff --git a/src/controllers/ProfileEmployeeAbsentLateController.ts b/src/controllers/ProfileEmployeeAbsentLateController.ts new file mode 100644 index 00000000..37ce4994 --- /dev/null +++ b/src/controllers/ProfileEmployeeAbsentLateController.ts @@ -0,0 +1,217 @@ +import { + Body, + Controller, + Get, + Patch, + Path, + Post, + Request, + Route, + Security, + Tags, +} from "tsoa"; +import { AppDataSource } from "../database/data-source"; +import { In } from "typeorm"; +import { + ProfileEmployeeAbsentLate, + CreateProfileEmployeeAbsentLate, + CreateProfileEmployeeAbsentLateBatch, + UpdateProfileEmployeeAbsentLate, +} from "../entities/ProfileEmployeeAbsentLate"; +import HttpSuccess from "../interfaces/http-success"; +import HttpStatus from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { RequestWithUser } from "../middlewares/user"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import permission from "../interfaces/permission"; +import { setLogDataDiff } from "../interfaces/utils"; + +@Route("api/v1/org/profile-employee/absent-late") +@Tags("ProfileEmployeeAbsentLate") +@Security("bearerAuth") +export class ProfileEmployeeAbsentLateController extends Controller { + private profileRepo = AppDataSource.getRepository(ProfileEmployee); + private absentLateRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLate); + + /** + * API ดึงข้อมูลการมาสาย/ขาดราชการของ user + * @summary API ดึงข้อมูลการมาสาย/ขาดราชการของ user + */ + @Get("user") + public async getAbsentLateUser(@Request() request: { user: Record }) { + const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + const record = await this.absentLateRepo.find({ + where: { profileEmployeeId: profile.id, isDeleted: false }, + order: { stampDate: "DESC" }, + }); + return new HttpSuccess(record); + } + + /** + * API ดึงข้อมูลการมาสาย/ขาดราชการตาม profileId + * @summary API ดึงข้อมูลการมาสาย/ขาดราชการตาม profileId + * @param profileId คีย์ profile + */ + @Get("{profileId}") + public async getAbsentLate(@Path() profileId: string, @Request() req: RequestWithUser) { + let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_EMP"); + if (_workflow == false) + await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileId); + const record = await this.absentLateRepo.find({ + where: { profileEmployeeId: profileId, isDeleted: false }, + order: { stampDate: "DESC" }, + }); + return new HttpSuccess(record); + } + + /** + * API สร้างข้อมูลการมาสาย/ขาดราชการ + * @summary API สร้างข้อมูลการมาสาย/ขาดราชการ + */ + @Post() + public async newAbsentLate( + @Request() req: RequestWithUser, + @Body() body: CreateProfileEmployeeAbsentLate, + ) { + if (!body.profileEmployeeId) { + throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileEmployeeId"); + } + + const profile = await this.profileRepo.findOneBy({ id: body.profileEmployeeId }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_EMP", profile.id); + + const before = null; + const data = new ProfileEmployeeAbsentLate(); + + const meta = { + 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, { ...body, ...meta }); + await this.absentLateRepo.save(data, { data: req }); + setLogDataDiff(req, { before, after: data }); + + return new HttpSuccess(data.id); + } + + /** + * API สร้างข้อมูลการมาสาย/ขาดราชการ (Batch) + * @summary API สร้างข้อมูลการมาสาย/ขาดราชการ (Batch) สำหรับ Job + */ + @Post("batch") + public async newAbsentLateBatch( + @Request() req: RequestWithUser, + @Body() body: CreateProfileEmployeeAbsentLateBatch, + ) { + if (!body.records || body.records.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณาระบุข้อมูลอย่างน้อย 1 รายการ"); + } + + const profileIds = [...new Set(body.records.map((r) => r.profileEmployeeId))]; + const profiles = await this.profileRepo.findBy({ + id: In(profileIds), + }); + + const foundProfileIds = new Set(profiles.map((p) => p.id)); + const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileEmployeeId)); + + if (validRecords.length === 0) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ที่ระบุ"); + } + + const meta = { + createdUserId: req.user.sub, + createdFullName: req.user.name, + lastUpdateUserId: req.user.sub, + lastUpdateFullName: req.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + const records = validRecords.map((item) => { + const data = new ProfileEmployeeAbsentLate(); + Object.assign(data, { ...item, ...meta }); + return data; + }); + + const result = await this.absentLateRepo.save(records, { data: req }); + + return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) }); + } + + /** + * API แก้ไขข้อมูลการมาสาย/ขาดราชการ + * @summary API แก้ไขข้อมูลการมาสาย/ขาดราชการ + * @param absentLateId คีย์การมาสาย/ขาดราชการ + */ + @Patch("{absentLateId}") + public async editAbsentLate( + @Request() req: RequestWithUser, + @Body() body: UpdateProfileEmployeeAbsentLate, + @Path() absentLateId: string, + ) { + const record = await this.absentLateRepo.findOneBy({ id: absentLateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + await new permission().PermissionOrgUserUpdate( + req, + "SYS_REGISTRY_EMP", + record.profileEmployeeId, + ); + + const before = structuredClone(record); + + Object.assign(record, body); + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = new Date(); + + await Promise.all([ + this.absentLateRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + ]); + + return new HttpSuccess(); + } + + /** + * API ลบข้อมูลการมาสาย/ขาดราชการ (Soft Delete) + * @summary API ลบข้อมูลการมาสาย/ขาดราชการ (Soft Delete) + * @param absentLateId คีย์การมาสาย/ขาดราชการ + */ + @Patch("update-delete/{absentLateId}") + public async updateIsDeleted( + @Request() req: RequestWithUser, + @Path() absentLateId: string, + ) { + const record = await this.absentLateRepo.findOneBy({ id: absentLateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (record.isDeleted === true) { + return new HttpSuccess(); + } + await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + + const before = structuredClone(record); + record.isDeleted = true; + record.lastUpdateUserId = req.user.sub; + record.lastUpdateFullName = req.user.name; + record.lastUpdatedAt = new Date(); + + await Promise.all([ + this.absentLateRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + ]); + + return new HttpSuccess(); + } +} diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index 8fc1275f..fe2f55d6 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -50,6 +50,7 @@ import { ProfileAssistance } from "./ProfileAssistance"; import { ProfileSalaryTemp } from "./ProfileSalaryTemp"; import { PositionSalaryEditHistory } from "./PositionSalaryEditHistory"; import { ProfileChangeName } from "./ProfileChangeName"; +import { ProfileAbsentLate } from "./ProfileAbsentLate"; @Entity("profile") export class Profile extends EntityBase { @@ -546,6 +547,9 @@ export class Profile extends EntityBase { @OneToMany(() => ProfileChangeName, (v) => v.profile) profileChangeNames: ProfileChangeName[]; + @OneToMany(() => ProfileAbsentLate, (v) => v.profile) + profileAbsentLates: ProfileAbsentLate[]; + @ManyToOne(() => PosLevel, (posLevel) => posLevel.profiles) @JoinColumn({ name: "posLevelId" }) posLevel: PosLevel; diff --git a/src/entities/ProfileAbsentLate.ts b/src/entities/ProfileAbsentLate.ts new file mode 100644 index 00000000..858412de --- /dev/null +++ b/src/entities/ProfileAbsentLate.ts @@ -0,0 +1,98 @@ +import { Entity, Column, ManyToOne, JoinColumn } from "typeorm"; +import { EntityBase } from "./base/Base"; +import { Profile } from "./Profile"; + +// Enums +export enum AbsentLateStatus { + LATE = "LATE", // มาสาย + ABSENT = "ABSENT", // ขาดราชการ +} + +export enum StampType { + FULL_DAY = "FULL_DAY", // เต็มวัน + MORNING = "MORNING", // ครึ่งเช้า + AFTERNOON = "AFTERNOON", // ครึ่งบ่าย +} + +@Entity("profileAbsentLate") +export class ProfileAbsentLate extends EntityBase { + @Column({ + nullable: true, + length: 40, + comment: "คีย์นอก(FK)ของตาราง Profile", + default: null, + }) + profileId: string; + + @Column({ + type: "enum", + enum: AbsentLateStatus, + comment: "สถานะ มาสาย/ขาดราชการ", + nullable: false, + }) + status: AbsentLateStatus; + + @Column({ + type: "datetime", + comment: "วันที่และเวลาที่ลงเวลา", + nullable: false, + }) + stampDate: Date; + + @Column({ + type: "enum", + enum: StampType, + comment: "เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย", + default: StampType.FULL_DAY, + }) + stampType: StampType; + + @Column({ + type: "decimal", + precision: 2, + scale: 1, + comment: "จำนวน (1.0/0.5)", + default: "1.0", + }) + stampAmount: number; + + @Column({ + type: "varchar", + length: 250, + comment: "หมายเหตุ", + nullable: true, + }) + remark: string; + + @Column({ + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + + @ManyToOne(() => Profile, (profile) => profile.profileAbsentLates) + @JoinColumn({ name: "profileId" }) + profile: Profile; +} + +// DTO Classes +export class CreateProfileAbsentLate { + profileId: string; + status: AbsentLateStatus; + stampDate: Date; + stampType?: StampType; + stampAmount?: number; + remark?: string; +} + +export class CreateProfileAbsentLateBatch { + records: CreateProfileAbsentLate[]; +} + +export type UpdateProfileAbsentLate = { + status?: AbsentLateStatus; + stampDate?: Date; + stampType?: StampType; + stampAmount?: number; + remark?: string; +}; diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index 50b6d78f..2c3f06e4 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -38,6 +38,7 @@ import { StateOperatorUser } from "./StateOperatorUser"; import { EmployeeTempPosMaster } from "./EmployeeTempPosMaster"; import { ProfileSalaryTemp } from "./ProfileSalaryTemp"; import { PositionSalaryEditHistory } from "./PositionSalaryEditHistory"; +import { ProfileEmployeeAbsentLate } from "./ProfileEmployeeAbsentLate"; @Entity("profileEmployee") export class ProfileEmployee extends EntityBase { @@ -827,6 +828,9 @@ export class ProfileEmployee extends EntityBase { @OneToMany(() => PositionSalaryEditHistory, (v) => v.profileEmployee) positionSalaryEditHistory: PositionSalaryEditHistory[]; + @OneToMany(() => ProfileEmployeeAbsentLate, (v) => v.profileEmployee) + profileEmployeeAbsentLates: ProfileEmployeeAbsentLate[]; + //ที่อยู่ @Column({ nullable: true, diff --git a/src/entities/ProfileEmployeeAbsentLate.ts b/src/entities/ProfileEmployeeAbsentLate.ts new file mode 100644 index 00000000..d7b8ea71 --- /dev/null +++ b/src/entities/ProfileEmployeeAbsentLate.ts @@ -0,0 +1,87 @@ +import { Entity, Column, ManyToOne, JoinColumn } from "typeorm"; +import { EntityBase } from "./base/Base"; +import { ProfileEmployee } from "./ProfileEmployee"; +import { AbsentLateStatus, StampType } from "./ProfileAbsentLate"; + +@Entity("profileEmployeeAbsentLate") +export class ProfileEmployeeAbsentLate extends EntityBase { + @Column({ + nullable: true, + length: 40, + comment: "คีย์นอก(FK)ของตาราง ProfileEmployee", + default: null, + }) + profileEmployeeId: string; + + @Column({ + type: "enum", + enum: AbsentLateStatus, + comment: "สถานะ มาสาย/ขาดราชการ", + nullable: false, + }) + status: AbsentLateStatus; + + @Column({ + type: "datetime", + comment: "วันที่และเวลาที่ลงเวลา", + nullable: false, + }) + stampDate: Date; + + @Column({ + type: "enum", + enum: StampType, + comment: "เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย", + default: StampType.FULL_DAY, + }) + stampType: StampType; + + @Column({ + type: "decimal", + precision: 2, + scale: 1, + comment: "จำนวน (1.0/0.5)", + default: "1.0", + }) + stampAmount: number; + + @Column({ + type: "varchar", + length: 250, + comment: "หมายเหตุ", + nullable: true, + }) + remark: string; + + @Column({ + comment: "สถานะลบข้อมูล", + default: false, + }) + isDeleted: boolean; + + @ManyToOne(() => ProfileEmployee, (profileEmployee) => profileEmployee.profileEmployeeAbsentLates) + @JoinColumn({ name: "profileEmployeeId" }) + profileEmployee: ProfileEmployee; +} + +// DTO Classes +export class CreateProfileEmployeeAbsentLate { + profileEmployeeId: string; + status: AbsentLateStatus; + stampDate: Date; + stampType?: StampType; + stampAmount?: number; + remark?: string; +} + +export class CreateProfileEmployeeAbsentLateBatch { + records: CreateProfileEmployeeAbsentLate[]; +} + +export type UpdateProfileEmployeeAbsentLate = { + status?: AbsentLateStatus; + stampDate?: Date; + stampType?: StampType; + stampAmount?: number; + remark?: string; +}; diff --git a/src/entities/ProfileLeave.ts b/src/entities/ProfileLeave.ts index f037e303..8ce08d94 100644 --- a/src/entities/ProfileLeave.ts +++ b/src/entities/ProfileLeave.ts @@ -107,6 +107,24 @@ export class ProfileLeave extends EntityBase { }) isDeleted: boolean; + @Column({ + nullable: true, + comment: "ประเภทย่อยการลา (เช่น ศึกษาต่อ, ฝึกอบรม, ปฎอบัติการวิจัย, ดูงาน)", + type: "varchar", + length: 255, + default: null, + }) + leaveSubTypeName: string; + + @Column({ + nullable: true, + comment: "ประเทศที่ไป", + type: "varchar", + length: 255, + default: null, + }) + coupleDayLevelCountry: string; + @OneToMany(() => ProfileLeaveHistory, (v) => v.profileLeave) histories: ProfileLeaveHistory[]; @@ -153,6 +171,8 @@ export class CreateProfileLeave { status?: string | null; reason: string | null; leaveId?: string | null; + leaveSubTypeName?: string | null; + coupleDayLevelCountry?: string | null; } export class CreateProfileEmployeeLeave { @@ -166,6 +186,8 @@ export class CreateProfileEmployeeLeave { status: string | null; reason: string | null; leaveId?: string | null; + leaveSubTypeName?: string | null; + coupleDayLevelCountry?: string | null; } export type UpdateProfileLeave = { @@ -177,4 +199,6 @@ export type UpdateProfileLeave = { totalLeave?: number | null; status?: string | null; reason?: string | null; + leaveSubTypeName?: string | null; + coupleDayLevelCountry?: string | null; }; diff --git a/src/migration/1774245696453-create_table_profileAbsentLate_.ts b/src/migration/1774245696453-create_table_profileAbsentLate_.ts new file mode 100644 index 00000000..11835568 --- /dev/null +++ b/src/migration/1774245696453-create_table_profileAbsentLate_.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateTableProfileAbsentLate_1774245696453 implements MigrationInterface { + name = 'CreateTableProfileAbsentLate_1774245696453' + + public async up(queryRunner: QueryRunner): Promise { + + await queryRunner.query(`CREATE TABLE \`profileAbsentLate\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง Profile', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`profileEmployeeAbsentLate\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileEmployeeId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileEmployee', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + + await queryRunner.query(`ALTER TABLE \`profileLeave\` ADD \`leaveSubTypeName\` varchar(255) NULL COMMENT 'ประเภทย่อยการลา (เช่น ศึกษาต่อ, ฝึกอบรม, ปฎอบัติการวิจัย, ดูงาน)'`); + await queryRunner.query(`ALTER TABLE \`profileLeave\` ADD \`coupleDayLevelCountry\` varchar(255) NULL COMMENT 'ประเทศที่ไป'`); + await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` ADD \`leaveSubTypeName\` varchar(255) NULL COMMENT 'ประเภทย่อยการลา (เช่น ศึกษาต่อ, ฝึกอบรม, ปฎอบัติการวิจัย, ดูงาน)'`); + await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` ADD \`coupleDayLevelCountry\` varchar(255) NULL COMMENT 'ประเทศที่ไป'`); + + await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` ADD CONSTRAINT \`FK_28f5579c548da2fd76b5295d8d5\` FOREIGN KEY (\`profileId\`) REFERENCES \`profile\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` ADD CONSTRAINT \`FK_f22d4ae4155cafd14e9c6d888e6\` FOREIGN KEY (\`profileEmployeeId\`) REFERENCES \`profileEmployee\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + + await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` DROP FOREIGN KEY \`FK_f22d4ae4155cafd14e9c6d888e6\``); + await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` DROP FOREIGN KEY \`FK_28f5579c548da2fd76b5295d8d5\``); + + await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` DROP COLUMN \`coupleDayLevelCountry\``); + await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` DROP COLUMN \`leaveSubTypeName\``); + await queryRunner.query(`ALTER TABLE \`profileLeave\` DROP COLUMN \`coupleDayLevelCountry\``); + await queryRunner.query(`ALTER TABLE \`profileLeave\` DROP COLUMN \`leaveSubTypeName\``); + + await queryRunner.query(`DROP TABLE \`profileEmployeeAbsentLate\``); + await queryRunner.query(`DROP TABLE \`profileAbsentLate\``); + } + +} diff --git a/src/migration/1774253766170-update_table_profileAbsentLate.ts b/src/migration/1774253766170-update_table_profileAbsentLate.ts new file mode 100644 index 00000000..be05ca8c --- /dev/null +++ b/src/migration/1774253766170-update_table_profileAbsentLate.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateTableProfileAbsentLate1774253766170 implements MigrationInterface { + name = 'UpdateTableProfileAbsentLate1774253766170' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` DROP COLUMN \`stampDate\``); + await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` ADD \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา'`); + await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` DROP COLUMN \`stampDate\``); + await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` ADD \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา'`); + } + + public async down(queryRunner: QueryRunner): Promise { + + await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` DROP COLUMN \`stampDate\``); + await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` ADD \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา'`); + await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` DROP COLUMN \`stampDate\``); + await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` ADD \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา'`); + } + +} From 9f9fd612d389f4806f64e60ac4d153cbe244fe8b Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 23 Mar 2026 17:44:11 +0700 Subject: [PATCH 157/178] fix report kk1 --- src/controllers/ProfileController.ts | 94 +++++++------------ .../ProfileEmployeeAbsentLateController.ts | 4 +- src/controllers/ProfileEmployeeController.ts | 70 ++++++-------- 3 files changed, 66 insertions(+), 102 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index ccb231a4..0eaf5a1f 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -32,7 +32,7 @@ import { UpdateProfileCouple, UpdatePrivacyDto, } from "../entities/Profile"; -import { Brackets, In, IsNull, Like, Not } from "typeorm"; +import { Brackets, In, IsNull, Like, Not, MoreThan } from "typeorm"; import { OrgRevision } from "../entities/OrgRevision"; import { PosMaster } from "../entities/PosMaster"; import { PosLevel } from "../entities/PosLevel"; @@ -1499,8 +1499,8 @@ export class ProfileController extends Controller { const position_raw = await this.salaryRepo.find({ where: { profileId: id, - commandCode: In(["1", "2", "3", "4", "8", "10", "11", "12", "15", "16", "20"]), - isEntry: false, + commandCode: In(["0","1","2","3","4","8","9","10","11","12","13","14","15","16","20"]), + // isEntry: false, }, order: { order: "ASC" }, }); @@ -1529,7 +1529,7 @@ export class ProfileController extends Controller { positionSalaryAmount: item.positionSalaryAmount ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1547,50 +1547,35 @@ export class ProfileController extends Controller { ]; // ประวัติพ้นจากราชการ - const retireCodes = ['C-PM-12','C-PM-13','C-PM-17','C-PM-18','C-PM-19','C-PM-20','C-PM-23','C-PM-43']; - let retires = []; - let inRetirePeriod = false; - let currentRetire = null; + const currentDate = new Date(); + const retire_raw = await this.salaryRepo.findOne({ + where: { + profileId: id, + commandCode: In(["16"]), + }, + order: { order: "desc" }, + }); - for (let i = 0; i < position_raw.length; i++) { - const item = position_raw[i]; - - if (retireCodes.includes(item.commandCode)) { - // เริ่มพ้นจากราชการ - currentRetire = { - commandName: item.commandName ?? "-", - startDate: item.commandDateAffect - }; - inRetirePeriod = true; + if (retire_raw) { + const startDate = retire_raw.commandDateAffect; + + // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน + let daysCount = 0; + if (startDate) { + const start = new Date(startDate); + daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); } - else if (inRetirePeriod && currentRetire) { - // เจอคำสั่งถัดไปที่ไม่ใช่การพ้นจากราชการ = วันกลับเข้า - const endDate = item.commandDateAffect; - let daysCount = 0; - if (currentRetire.startDate && endDate) { - const start = new Date(currentRetire.startDate); - const end = new Date(endDate); - daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - } + const startDateStr = startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + : "-"; - const startDateStr = currentRetire.startDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(currentRetire.startDate)) - : "-"; - const endDateStr = endDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) - : "-"; - - retires.push({ - date: `${startDateStr} - ${endDateStr}`, - detail: currentRetire.commandName, - day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - }); - - inRetirePeriod = false; - currentRetire = null; - } + retires.push({ + date: `${startDateStr}`, + detail: retire_raw.commandName ?? "-", + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + }); } // กรณีไม่มีข้อมูล @@ -1620,11 +1605,7 @@ export class ProfileController extends Controller { commandExcecuteDate, } = await getPosNumCodeSit(item.commandId, "REPORTED"); - _document = Extension.ToThaiNumber( - `คำสั่ง ${posNumCodeSitAbb} - ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} - ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))}` - ) + _document = Extension.ToThaiNumber(`คำสั่ง ${posNumCodeSitAbb ?? "-"} ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))}`) _commandTypename = commandTypeName; } // ค้นหาหน่วยงานที่รักษาการ @@ -1656,7 +1637,7 @@ export class ProfileController extends Controller { } _actpositions.push({ - data: + date: item.dateStart && item.dateEnd ? Extension.ToThaiNumber( `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, @@ -1689,14 +1670,14 @@ export class ProfileController extends Controller { // ช่วยราชการ const assistance_raw = await this.profileAssistanceRepository.find({ select: ["dateStart", "dateEnd", "commandName", "agency", "document", "isDeleted"], - where: { profileId: id, status: "PENDING", isDeleted: false }, + where: { profileId: id, /*status: "PENDING",*/ isDeleted: false }, order: { createdAt: "ASC" }, }); let _assistances = [] if (assistance_raw.length > 0) { for (const item of assistance_raw) { let _commandTypename: string = item.commandName ?? "ให้ช่วยราชการ"; - let _document: string = "-"; + let _document: string = item.document ? Extension.ToThaiNumber(item.document) : "-"; let _org: string = item.agency ? Extension.ToThaiNumber(item.agency) : "-"; if (item.commandId) { @@ -1708,15 +1689,11 @@ export class ProfileController extends Controller { commandExcecuteDate, } = await getPosNumCodeSit(item.commandId); - _document = Extension.ToThaiNumber(` - คำสั่ง ${posNumCodeSitAbb} - ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} - ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))} - `) + _document = Extension.ToThaiNumber(`คำสั่ง ${posNumCodeSitAbb ?? "-"} ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))}`) _commandTypename = commandTypeName; } _assistances.push({ - data: + date: item.dateStart && item.dateEnd ? Extension.ToThaiNumber( `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, @@ -1860,7 +1837,7 @@ export class ProfileController extends Controller { ? Extension.ToThaiNumber(item.positionCee) : null, amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1955,6 +1932,7 @@ export class ProfileController extends Controller { order: { createdAt: "ASC" }, }); const data = { + currentDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(currentDate)), fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, prefix: profiles?.prefix != null ? profiles.prefix : "", firstName: profiles?.firstName != null ? profiles.firstName : "", diff --git a/src/controllers/ProfileEmployeeAbsentLateController.ts b/src/controllers/ProfileEmployeeAbsentLateController.ts index 37ce4994..a5e32fa1 100644 --- a/src/controllers/ProfileEmployeeAbsentLateController.ts +++ b/src/controllers/ProfileEmployeeAbsentLateController.ts @@ -106,8 +106,8 @@ export class ProfileEmployeeAbsentLateController extends Controller { } /** - * API สร้างข้อมูลการมาสาย/ขาดราชการ (Batch) - * @summary API สร้างข้อมูลการมาสาย/ขาดราชการ (Batch) สำหรับ Job + * API สร้างข้อมูลการมาสาย/ขาดราชการ (สำหรับ Job) + * @summary API สร้างข้อมูลการมาสาย/ขาดราชการ (สำหรับ Job) */ @Post("batch") public async newAbsentLateBatch( diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 89293c5d..39b6ea46 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1493,8 +1493,8 @@ export class ProfileEmployeeController extends Controller { const position_raw = await this.salaryRepo.find({ where: { profileEmployeeId: id, - commandCode: In(["1", "2", "3", "4", "8", "10", "11", "12", "15", "16", "20"]), - isEntry: false, + commandCode: In(["0","1","2","3","4","8","9","10","11","12","13","14","15","16","20"]), + // isEntry: false, }, order: { order: "ASC" }, }); @@ -1523,7 +1523,7 @@ export class ProfileEmployeeController extends Controller { positionSalaryAmount: item.positionSalaryAmount ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1734,7 +1734,7 @@ export class ProfileEmployeeController extends Controller { ? Extension.ToThaiNumber(item.positionCee) : null, amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb} ที่ ${item.commandNo}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) })) : [ { @@ -1750,50 +1750,35 @@ export class ProfileEmployeeController extends Controller { ]; // ประวัติพ้นจากราชการ - const retireCodes = ['C-PM-12','C-PM-13','C-PM-17','C-PM-18','C-PM-19','C-PM-20','C-PM-23','C-PM-43']; - let retires = []; - let inRetirePeriod = false; - let currentRetire = null; + const currentDate = new Date(); + const retire_raw = await this.salaryRepo.findOne({ + where: { + profileEmployeeId: id, + commandCode: In(["16"]), + }, + order: { order: "desc" }, + }); - for (let i = 0; i < position_raw.length; i++) { - const item = position_raw[i]; + if (retire_raw) { + const startDate = retire_raw.commandDateAffect; - if (retireCodes.includes(item.commandCode)) { - // เริ่มพ้นจากราชการ - currentRetire = { - commandName: item.commandName ?? "-", - startDate: item.commandDateAffect - }; - inRetirePeriod = true; + // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน + let daysCount = 0; + if (startDate) { + const start = new Date(startDate); + daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); } - else if (inRetirePeriod && currentRetire) { - // เจอคำสั่งถัดไปที่ไม่ใช่การพ้นจากราชการ = วันกลับเข้า - const endDate = item.commandDateAffect; - let daysCount = 0; - if (currentRetire.startDate && endDate) { - const start = new Date(currentRetire.startDate); - const end = new Date(endDate); - daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - } + const startDateStr = startDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + : "-"; - const startDateStr = currentRetire.startDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(currentRetire.startDate)) - : "-"; - const endDateStr = endDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) - : "-"; - - retires.push({ - date: `${startDateStr} - ${endDateStr}`, - detail: currentRetire.commandName, - day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - }); - - inRetirePeriod = false; - currentRetire = null; - } + retires.push({ + date: `${startDateStr} - ปัจจุบัน`, + detail: retire_raw.commandName ?? "-", + day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + }); } // กรณีไม่มีข้อมูล @@ -1865,6 +1850,7 @@ export class ProfileEmployeeController extends Controller { ) : ""; const data = { + currentDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(currentDate)), fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, prefix: profiles?.prefix != null ? profiles.prefix : "", firstName: profiles?.firstName != null ? profiles.firstName : "", From d83c8241fa07163a436499628d0af755a334bc36 Mon Sep 17 00:00:00 2001 From: Suphonchai Phoonsawat Date: Tue, 24 Mar 2026 15:25:11 +0700 Subject: [PATCH 158/178] Implement feature X to enhance user experience and fix bug Y in module Z --- package-lock.json | 3473 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 3459 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 05c9f107..b12df16f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,12 +43,15 @@ "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", "@types/node": "^20.11.5", "@types/node-cron": "^3.0.11", "@types/swagger-ui-express": "^4.1.6", "@types/ws": "^8.5.14", + "jest": "^29.7.0", "nodemon": "^3.0.3", "prettier": "^3.2.2", + "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "typescript": "^5.3.3" } @@ -92,6 +95,600 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -175,6 +772,523 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jest/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/reporters/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -185,10 +1299,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "devOptional": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -485,6 +1600,33 @@ "node": ">=14" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -591,6 +1733,51 @@ "@types/node": "*" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -638,11 +1825,59 @@ "@types/send": "*" } }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -699,6 +1934,13 @@ "@types/node": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/swagger-ui-express": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", @@ -718,6 +1960,23 @@ "@types/node": "*" } }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/zen-observable": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", @@ -992,6 +2251,132 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1025,6 +2410,19 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", @@ -1080,17 +2478,75 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1190,6 +2646,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", @@ -1198,6 +2664,27 @@ "node": ">=0.10.0" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -1244,6 +2731,16 @@ "node": ">=8" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -1276,6 +2773,29 @@ "fsevents": "~2.3.2" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -1466,6 +2986,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1556,6 +3094,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -1588,6 +3133,28 @@ "node": ">= 0.10" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1642,6 +3209,21 @@ "node": ">=0.10.0" } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1712,6 +3294,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1720,6 +3312,16 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dotenv": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", @@ -1810,6 +3412,26 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1906,6 +3528,16 @@ } } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -2063,9 +3695,10 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2084,6 +3717,20 @@ "node": ">=0.8.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2093,6 +3740,89 @@ "node": ">= 0.6" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -2191,6 +3921,13 @@ "node": ">=4" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-jwt": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-3.3.2.tgz", @@ -2229,6 +3966,16 @@ "node": ">= 4.9.1" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -2241,10 +3988,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2270,6 +4018,20 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -2424,6 +4186,16 @@ "is-property": "^1.0.2" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2456,6 +4228,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -2469,6 +4251,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -2682,6 +4477,13 @@ "node": ">=14" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2697,6 +4499,16 @@ "node": ">= 0.8" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2733,6 +4545,36 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2935,6 +4777,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -2989,6 +4838,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -3020,6 +4885,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3056,6 +4931,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -3113,6 +4989,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -3187,6 +5076,125 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", @@ -3233,6 +5241,683 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/js-beautify": { "version": "1.14.11", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz", @@ -3295,6 +5980,39 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -3362,6 +6080,16 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", @@ -3370,6 +6098,36 @@ "node": ">=0.10.0" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -3405,6 +6163,13 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -3442,11 +6207,37 @@ "node": ">=16.14" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3487,6 +6278,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3495,6 +6293,20 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3719,6 +6531,13 @@ "node": ">=12" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3771,6 +6590,20 @@ } } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, "node_modules/node-xlsx": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/node-xlsx/-/node-xlsx-0.24.0.tgz", @@ -3860,6 +6693,19 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3970,6 +6816,80 @@ "node": ">=0.10.0" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -3997,6 +6917,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4013,6 +6943,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", @@ -4042,6 +6979,13 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4054,6 +6998,29 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prettier": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", @@ -4069,6 +7036,34 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -4094,6 +7089,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -4122,6 +7131,23 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4173,6 +7199,13 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -4301,6 +7334,60 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -4656,6 +7743,23 @@ "node": ">=10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -4817,6 +7921,29 @@ "node": ">= 0.6" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -4857,6 +7984,43 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4987,6 +8151,39 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4998,6 +8195,19 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/swagger-ui-dist": { "version": "5.11.0", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.0.tgz", @@ -5087,6 +8297,43 @@ "window-size": "0.1.0" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -5122,11 +8369,19 @@ "node": ">=0.6.0" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -5160,6 +8415,72 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "peer": true }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -5224,6 +8545,29 @@ "yarn": ">=1.9.4" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -5829,6 +9173,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -5869,6 +9244,32 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/validator": { "version": "13.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", @@ -5885,6 +9286,16 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -6055,6 +9466,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", @@ -6230,6 +9662,19 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", From e7b1bb13bbd182242828180e4d183ce54499a925 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 24 Mar 2026 16:44:38 +0700 Subject: [PATCH 159/178] =?UTF-8?q?=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=87?= =?UTF-8?q?=E0=B8=B2=E0=B8=99=20=E0=B8=97=E0=B8=9B=E0=B8=AD.=E0=B8=AA?= =?UTF-8?q?=E0=B8=B2=E0=B8=A1=E0=B8=B1=E0=B8=8D=20(=E0=B9=81=E0=B8=81?= =?UTF-8?q?=E0=B9=89=E0=B9=84=E0=B8=82=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=E0=B9=80=E0=B8=95=E0=B8=B4=E0=B8=A1)=20#2360?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileController.ts | 93 +++++++++++++++++--- src/controllers/ProfileEmployeeController.ts | 93 +++++++++++++++++--- 2 files changed, 164 insertions(+), 22 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 0eaf5a1f..84702d36 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -61,6 +61,7 @@ import { ProfileFamilyFather } from "../entities/ProfileFamilyFather"; import Extension from "../interfaces/extension"; import { ProfileInsignia } from "../entities/ProfileInsignia"; import { ProfileLeave } from "../entities/ProfileLeave"; +import { ProfileAbsentLate } from "../entities/ProfileAbsentLate"; import { updateName, deleteUser } from "../keycloak"; import permission from "../interfaces/permission"; import { PosMasterAct } from "../entities/PosMasterAct"; @@ -134,6 +135,7 @@ export class ProfileController extends Controller { private subDistrictRepo = AppDataSource.getRepository(SubDistrict); private profileInsigniaRepo = AppDataSource.getRepository(ProfileInsignia); private profileLeaveRepository = AppDataSource.getRepository(ProfileLeave); + private profileAbsentLateRepo = AppDataSource.getRepository(ProfileAbsentLate); private posMasterActRepository = AppDataSource.getRepository(PosMasterAct); private profileChildrenRepository = AppDataSource.getRepository(ProfileChildren); private changeNameRepository = AppDataSource.getRepository(ProfileChangeName); @@ -1389,16 +1391,70 @@ export class ProfileController extends Controller { leaves.push({year:""}); } + // Query มาสาย/ขาดราชการ และ merge ตามปี + const absentLate_raw = await this.profileAbsentLateRepo + .createQueryBuilder("absentLate") + .select([ + "YEAR(absentLate.stampDate) as year", + "absentLate.status as status", + "SUM(absentLate.stampAmount) as totalAmount", + ]) + .where("absentLate.profileId = :profileId", { profileId: id }) + .andWhere("absentLate.isDeleted = :isDeleted", { isDeleted: false }) + .groupBy("YEAR(absentLate.stampDate), absentLate.status") + .orderBy("year", "DESC") + .getRawMany(); + + // Merge มาสาย/ขาดราชการเข้า leaves array + absentLate_raw.forEach((item) => { + const year = item.year + ? Extension.ToThaiNumber((item.year + 543).toString()) + : ""; + + let yearData = leaves.find((data) => data.year === year); + + // ถ้าไม่มีปีนั้นใน leaves ให้สร้างใหม่ + if (!yearData) { + yearData = { year }; + for (let i = 1; i <= 11; i++) { + yearData[`leaveTypeCodeLv${i}`] = "-"; + yearData[`totalLeaveDaysLv${i}`] = "-"; + yearData[`leaveTypeNameLv${i}`] = "-"; + } + leaves.push(yearData); + } + + // เพิ่มข้อมูลมาสาย/ขาดราชการ + if (item.status === "LATE") { + yearData.late = item.totalAmount + ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) + : "-"; + } else if (item.status === "ABSENT") { + yearData.absent = item.totalAmount + ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) + : "-"; + } + }); + + // เติมค่า "-" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ + leaves.forEach((yearData) => { + if (!yearData.late) yearData.late = "-"; + if (!yearData.absent) yearData.absent = "-"; + }); + const leave2_raw = await this.profileLeaveRepository .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") .select([ + "profileLeave.leaveSubTypeName AS leaveSubTypeName", + "profileLeave.coupleDayLevelCountry AS coupleDayLevelCountry", "profileLeave.isDeleted AS isDeleted", "profileLeave.dateLeaveStart AS dateLeaveStart", "profileLeave.dateLeaveEnd AS dateLeaveEnd", "profileLeave.leaveDays AS leaveDays", "profileLeave.reason AS reason", "leaveType.name as name", + "leaveType.code as code", ]) .where("profileLeave.profileId = :profileId", { profileId: id }) .andWhere("profileLeave.isDeleted = :isDeleted", { isDeleted: false }) @@ -1408,17 +1464,32 @@ export class ProfileController extends Controller { .getRawMany(); const leaves2 = leave2_raw.length > 0 - ? leave2_raw.map((item) => ({ - date: - item.dateLeaveStart && item.dateLeaveEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + - " - " + - Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveEnd)) - : "-", - type: item.name || "-", - leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "-", - reason: item.reason || "-", - })) + ? leave2_raw.map((item) => { + const leaveTypeCode = item.code ? item.code.trim().toUpperCase() : ""; + + // ข้อที่ 1: LV-008 ให้ใช้ leaveSubTypeName (ประเภทย่อย) แทน name + const displayType = + leaveTypeCode === "LV-008" && item.leaveSubTypeName + ? item.leaveSubTypeName + : (item.name || "-"); + + // ข้อที่ 2: แสดง reason ก่อนเสมอ ถ้ามี coupleDayLevelCountry ค่อยแสดงประเทศ + const displayReason = item.coupleDayLevelCountry + ? `${item.reason || ""} ลาไปประเทศ ${item.coupleDayLevelCountry}`.trim() + : (item.reason || "-"); + + return { + date: + item.dateLeaveStart && item.dateLeaveEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + + " - " + + Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveEnd)) + : "-", + type: displayType, + leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "-", + reason: displayReason, + }; + }) : [ { date: "", diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 39b6ea46..8b605b56 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -66,6 +66,7 @@ import { ProfileEmployeeEmploymentHistory } from "../entities/ProfileEmployeeEmp import CallAPI from "../interfaces/call-api"; import { ProfileInsignia } from "../entities/ProfileInsignia"; import { ProfileLeave } from "../entities/ProfileLeave"; +import { ProfileEmployeeAbsentLate } from "../entities/ProfileEmployeeAbsentLate"; import permission from "../interfaces/permission"; import axios from "axios"; import { Position } from "../entities/Position"; @@ -124,6 +125,7 @@ export class ProfileEmployeeController extends Controller { private profileEducationRepo = AppDataSource.getRepository(ProfileEducation); private profileInsigniaRepo = AppDataSource.getRepository(ProfileInsignia); private profileLeaveRepository = AppDataSource.getRepository(ProfileLeave); + private profileEmployeeAbsentLateRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLate); private positionRepository = AppDataSource.getRepository(Position); private employeePositionRepository = AppDataSource.getRepository(EmployeePosition); private permissionProflileRepository = AppDataSource.getRepository(PermissionProfile); @@ -1383,16 +1385,70 @@ export class ProfileEmployeeController extends Controller { leaves.push({year:""}); } + // Query มาสาย/ขาดราชการ และ merge ตามปี + const absentLate_raw = await this.profileEmployeeAbsentLateRepo + .createQueryBuilder("absentLate") + .select([ + "YEAR(absentLate.stampDate) as year", + "absentLate.status as status", + "SUM(absentLate.stampAmount) as totalAmount", + ]) + .where("absentLate.profileEmployeeId = :profileId", { profileId: id }) + .andWhere("absentLate.isDeleted = :isDeleted", { isDeleted: false }) + .groupBy("YEAR(absentLate.stampDate), absentLate.status") + .orderBy("year", "DESC") + .getRawMany(); + + // Merge มาสาย/ขาดราชการเข้า leaves array + absentLate_raw.forEach((item) => { + const year = item.year + ? Extension.ToThaiNumber((item.year + 543).toString()) + : ""; + + let yearData = leaves.find((data) => data.year === year); + + // ถ้าไม่มีปีนั้นใน leaves ให้สร้างใหม่ + if (!yearData) { + yearData = { year }; + for (let i = 1; i <= 11; i++) { + yearData[`leaveTypeCodeLv${i}`] = "-"; + yearData[`totalLeaveDaysLv${i}`] = "-"; + yearData[`leaveTypeNameLv${i}`] = "-"; + } + leaves.push(yearData); + } + + // เพิ่มข้อมูลมาสาย/ขาดราชการ + if (item.status === "LATE") { + yearData.lateAmount = item.totalAmount + ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) + : "-"; + } else if (item.status === "ABSENT") { + yearData.absentAmount = item.totalAmount + ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) + : "-"; + } + }); + + // เติมค่า "-" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ + leaves.forEach((yearData) => { + if (!yearData.lateAmount) yearData.lateAmount = "-"; + if (!yearData.absentAmount) yearData.absentAmount = "-"; + }); + const leave2_raw = await this.profileLeaveRepository .createQueryBuilder("profileLeave") .leftJoinAndSelect("profileLeave.leaveType", "leaveType") .select([ + "profileLeave.leaveSubTypeName AS leaveSubTypeName", + "profileLeave.coupleDayLevelCountry AS coupleDayLevelCountry", "profileLeave.isDeleted AS isDeleted", "profileLeave.dateLeaveStart AS dateLeaveStart", "profileLeave.dateLeaveEnd AS dateLeaveEnd", "profileLeave.leaveDays AS leaveDays", "profileLeave.reason AS reason", "leaveType.name as name", + "leaveType.code as code", ]) .where("profileLeave.profileEmployeeId = :profileId", { profileId: id }) .andWhere("profileLeave.isDeleted = :isDeleted", { isDeleted: false }) @@ -1402,17 +1458,32 @@ export class ProfileEmployeeController extends Controller { .getRawMany(); const leaves2 = leave2_raw.length > 0 - ? leave2_raw.map((item) => ({ - date: - item.dateLeaveStart && item.dateLeaveEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + - " - " + - Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveEnd)) - : "-", - type: item.name || "-", - leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "-", - reason: item.reason || "-", - })) + ? leave2_raw.map((item) => { + const leaveTypeCode = item.code ? item.code.trim().toUpperCase() : ""; + + // ข้อที่ 1: LV-008 ให้ใช้ leaveSubTypeName (ประเภทย่อย) แทน name + const displayType = + leaveTypeCode === "LV-008" && item.leaveSubTypeName + ? item.leaveSubTypeName + : (item.name || "-"); + + // ข้อที่ 2: แสดง reason ก่อนเสมอ ถ้ามี coupleDayLevelCountry ค่อยแสดงประเทศ + const displayReason = item.coupleDayLevelCountry + ? `${item.reason || ""} ลาไปประเทศ ${item.coupleDayLevelCountry}`.trim() + : (item.reason || "-"); + + return { + date: + item.dateLeaveStart && item.dateLeaveEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + + " - " + + Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveEnd)) + : "-", + type: displayType, + leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "-", + reason: displayReason, + }; + }) : [ { date: "", From ecb3cb1d2a6ff3e6c1dee136dc0e204abc5c3a71 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 24 Mar 2026 17:47:45 +0700 Subject: [PATCH 160/178] no message --- src/controllers/ProfileController.ts | 2 +- src/controllers/ProfileEmployeeController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 84702d36..ccec5b1a 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1623,7 +1623,7 @@ export class ProfileController extends Controller { const retire_raw = await this.salaryRepo.findOne({ where: { profileId: id, - commandCode: In(["16"]), + commandCode: In(["12", "15", "16"]), }, order: { order: "desc" }, }); diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 8b605b56..c476e3ae 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1826,7 +1826,7 @@ export class ProfileEmployeeController extends Controller { const retire_raw = await this.salaryRepo.findOne({ where: { profileEmployeeId: id, - commandCode: In(["16"]), + commandCode: In(["12", "15", "16"]), }, order: { order: "desc" }, }); From ef0d6ea1b51bce4a6c307bdca16915fee5385d6f Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 25 Mar 2026 11:48:22 +0700 Subject: [PATCH 161/178] Migrate add absentLateHistory --- .../ProfileAbsentLateController.ts | 64 ++++++++++++++++++- .../ProfileEmployeeAbsentLateController.ts | 64 ++++++++++++++++++- src/entities/ProfileAbsentLateHistory.ts | 20 ++++++ .../ProfileEmployeeAbsentLateHistory.ts | 17 +++++ ...74408245407-add_table_absentLateHistory.ts | 19 ++++++ 5 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/entities/ProfileAbsentLateHistory.ts create mode 100644 src/entities/ProfileEmployeeAbsentLateHistory.ts create mode 100644 src/migration/1774408245407-add_table_absentLateHistory.ts diff --git a/src/controllers/ProfileAbsentLateController.ts b/src/controllers/ProfileAbsentLateController.ts index b9e64a8f..ef977097 100644 --- a/src/controllers/ProfileAbsentLateController.ts +++ b/src/controllers/ProfileAbsentLateController.ts @@ -18,6 +18,7 @@ import { CreateProfileAbsentLateBatch, UpdateProfileAbsentLate, } from "../entities/ProfileAbsentLate"; +import { ProfileAbsentLateHistory } from "../entities/ProfileAbsentLateHistory"; import HttpSuccess from "../interfaces/http-success"; import HttpStatus from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; @@ -32,6 +33,7 @@ import { setLogDataDiff } from "../interfaces/utils"; export class ProfileAbsentLateController extends Controller { private profileRepo = AppDataSource.getRepository(Profile); private absentLateRepo = AppDataSource.getRepository(ProfileAbsentLate); + private historyRepo = AppDataSource.getRepository(ProfileAbsentLateHistory); /** * API ดึงข้อมูลการมาสาย/ขาดราชการของ user @@ -99,8 +101,15 @@ export class ProfileAbsentLateController extends Controller { }; Object.assign(data, { ...body, ...meta }); + + // บันทึก history + const history = new ProfileAbsentLateHistory(); + Object.assign(history, { ...data, id: undefined }); + await this.absentLateRepo.save(data, { data: req }); setLogDataDiff(req, { before, after: data }); + history.profileAbsentLateId = data.id; + await this.historyRepo.save(history, { data: req }); return new HttpSuccess(data.id); } @@ -114,8 +123,9 @@ export class ProfileAbsentLateController extends Controller { @Request() req: RequestWithUser, @Body() body: CreateProfileAbsentLateBatch, ) { + // กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย) if (!body.records || body.records.length === 0) { - throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณาระบุข้อมูลอย่างน้อย 1 รายการ"); + return new HttpSuccess({ count: 0, ids: [] }); } const profileIds = [...new Set(body.records.map((r) => r.profileId))]; @@ -126,8 +136,9 @@ export class ProfileAbsentLateController extends Controller { const foundProfileIds = new Set(profiles.map((p) => p.id)); const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileId)); + // กรณีไม่พบ profile เลย if (validRecords.length === 0) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ที่ระบุ"); + return new HttpSuccess({ count: 0, ids: [] }); } const meta = { @@ -147,6 +158,15 @@ export class ProfileAbsentLateController extends Controller { const result = await this.absentLateRepo.save(records, { data: req }); + // บันทึก history สำหรับแต่ละ record + const historyRecords = result.map((data) => { + const history = new ProfileAbsentLateHistory(); + Object.assign(history, { ...data, id: undefined }); + history.profileAbsentLateId = data.id; + return history; + }); + await this.historyRepo.save(historyRecords, { data: req }); + return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) }); } @@ -170,15 +190,27 @@ export class ProfileAbsentLateController extends Controller { ); const before = structuredClone(record); + const history = new ProfileAbsentLateHistory(); + Object.assign(history, { ...record, id: undefined }); Object.assign(record, body); + Object.assign(history, { ...record, id: undefined }); + + history.profileAbsentLateId = absentLateId; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; record.lastUpdatedAt = new Date(); + history.lastUpdateUserId = req.user.sub; + history.lastUpdateFullName = req.user.name; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = new Date(); + history.lastUpdatedAt = new Date(); await Promise.all([ this.absentLateRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), + this.historyRepo.save(history, { data: req }), ]); return new HttpSuccess(); @@ -202,16 +234,44 @@ export class ProfileAbsentLateController extends Controller { await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId); const before = structuredClone(record); + const history = new ProfileAbsentLateHistory(); + Object.assign(history, { ...record, id: undefined }); + record.isDeleted = true; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; record.lastUpdatedAt = new Date(); + history.profileAbsentLateId = absentLateId; + history.isDeleted = true; + history.lastUpdateUserId = req.user.sub; + history.lastUpdateFullName = req.user.name; + history.lastUpdatedAt = new Date(); + await Promise.all([ this.absentLateRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), + this.historyRepo.save(history, { data: req }), ]); return new HttpSuccess(); } + + /** + * API ดึงประวัติการมาสาย/ขาดราชการ + * @summary API ดึงประวัติการมาสาย/ขาดราชการ + * @param absentLateId คีย์การมาสาย/ขาดราชการ + */ + @Get("history/{absentLateId}") + public async getHistory(@Path() absentLateId: string, @Request() req: RequestWithUser) { + const record = await this.absentLateRepo.findOneBy({ id: absentLateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", record.profileId); + + const history = await this.historyRepo.find({ + where: { profileAbsentLateId: absentLateId }, + order: { createdAt: "DESC" }, + }); + return new HttpSuccess(history); + } } diff --git a/src/controllers/ProfileEmployeeAbsentLateController.ts b/src/controllers/ProfileEmployeeAbsentLateController.ts index a5e32fa1..cdd67050 100644 --- a/src/controllers/ProfileEmployeeAbsentLateController.ts +++ b/src/controllers/ProfileEmployeeAbsentLateController.ts @@ -18,6 +18,7 @@ import { CreateProfileEmployeeAbsentLateBatch, UpdateProfileEmployeeAbsentLate, } from "../entities/ProfileEmployeeAbsentLate"; +import { ProfileEmployeeAbsentLateHistory } from "../entities/ProfileEmployeeAbsentLateHistory"; import HttpSuccess from "../interfaces/http-success"; import HttpStatus from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; @@ -32,6 +33,7 @@ import { setLogDataDiff } from "../interfaces/utils"; export class ProfileEmployeeAbsentLateController extends Controller { private profileRepo = AppDataSource.getRepository(ProfileEmployee); private absentLateRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLate); + private historyRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLateHistory); /** * API ดึงข้อมูลการมาสาย/ขาดราชการของ user @@ -99,8 +101,15 @@ export class ProfileEmployeeAbsentLateController extends Controller { }; Object.assign(data, { ...body, ...meta }); + + // บันทึก history + const history = new ProfileEmployeeAbsentLateHistory(); + Object.assign(history, { ...data, id: undefined }); + await this.absentLateRepo.save(data, { data: req }); setLogDataDiff(req, { before, after: data }); + history.profileEmployeeAbsentLateId = data.id; + await this.historyRepo.save(history, { data: req }); return new HttpSuccess(data.id); } @@ -114,8 +123,9 @@ export class ProfileEmployeeAbsentLateController extends Controller { @Request() req: RequestWithUser, @Body() body: CreateProfileEmployeeAbsentLateBatch, ) { + // กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย) if (!body.records || body.records.length === 0) { - throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณาระบุข้อมูลอย่างน้อย 1 รายการ"); + return new HttpSuccess({ count: 0, ids: [] }); } const profileIds = [...new Set(body.records.map((r) => r.profileEmployeeId))]; @@ -126,8 +136,9 @@ export class ProfileEmployeeAbsentLateController extends Controller { const foundProfileIds = new Set(profiles.map((p) => p.id)); const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileEmployeeId)); + // กรณีไม่พบ profile เลย if (validRecords.length === 0) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ที่ระบุ"); + return new HttpSuccess({ count: 0, ids: [] }); } const meta = { @@ -147,6 +158,15 @@ export class ProfileEmployeeAbsentLateController extends Controller { const result = await this.absentLateRepo.save(records, { data: req }); + // บันทึก history สำหรับแต่ละ record + const historyRecords = result.map((data) => { + const history = new ProfileEmployeeAbsentLateHistory(); + Object.assign(history, { ...data, id: undefined }); + history.profileEmployeeAbsentLateId = data.id; + return history; + }); + await this.historyRepo.save(historyRecords, { data: req }); + return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) }); } @@ -170,15 +190,27 @@ export class ProfileEmployeeAbsentLateController extends Controller { ); const before = structuredClone(record); + const history = new ProfileEmployeeAbsentLateHistory(); + Object.assign(history, { ...record, id: undefined }); Object.assign(record, body); + Object.assign(history, { ...record, id: undefined }); + + history.profileEmployeeAbsentLateId = absentLateId; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; record.lastUpdatedAt = new Date(); + history.lastUpdateUserId = req.user.sub; + history.lastUpdateFullName = req.user.name; + history.createdUserId = req.user.sub; + history.createdFullName = req.user.name; + history.createdAt = new Date(); + history.lastUpdatedAt = new Date(); await Promise.all([ this.absentLateRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), + this.historyRepo.save(history, { data: req }), ]); return new HttpSuccess(); @@ -202,16 +234,44 @@ export class ProfileEmployeeAbsentLateController extends Controller { await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); const before = structuredClone(record); + const history = new ProfileEmployeeAbsentLateHistory(); + Object.assign(history, { ...record, id: undefined }); + record.isDeleted = true; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; record.lastUpdatedAt = new Date(); + history.profileEmployeeAbsentLateId = absentLateId; + history.isDeleted = true; + history.lastUpdateUserId = req.user.sub; + history.lastUpdateFullName = req.user.name; + history.lastUpdatedAt = new Date(); + await Promise.all([ this.absentLateRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), + this.historyRepo.save(history, { data: req }), ]); return new HttpSuccess(); } + + /** + * API ดึงประวัติการมาสาย/ขาดราชการ + * @summary API ดึงประวัติการมาสาย/ขาดราชการ + * @param absentLateId คีย์การมาสาย/ขาดราชการ + */ + @Get("history/{absentLateId}") + public async getHistory(@Path() absentLateId: string, @Request() req: RequestWithUser) { + const record = await this.absentLateRepo.findOneBy({ id: absentLateId }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", record.profileEmployeeId); + + const history = await this.historyRepo.find({ + where: { profileEmployeeAbsentLateId: absentLateId }, + order: { createdAt: "DESC" }, + }); + return new HttpSuccess(history); + } } diff --git a/src/entities/ProfileAbsentLateHistory.ts b/src/entities/ProfileAbsentLateHistory.ts new file mode 100644 index 00000000..7059bb23 --- /dev/null +++ b/src/entities/ProfileAbsentLateHistory.ts @@ -0,0 +1,20 @@ +import { Entity, Column } from "typeorm"; +import { + ProfileAbsentLate, + AbsentLateStatus, + StampType, +} from "./ProfileAbsentLate"; + +@Entity("profileAbsentLateHistory") +export class ProfileAbsentLateHistory extends ProfileAbsentLate { + @Column({ + nullable: true, + length: 40, + comment: "คีย์นอก(FK)ของตาราง ProfileAbsentLate", + default: null, + }) + profileAbsentLateId: string; +} + +// Export enums for re-use +export { AbsentLateStatus, StampType }; diff --git a/src/entities/ProfileEmployeeAbsentLateHistory.ts b/src/entities/ProfileEmployeeAbsentLateHistory.ts new file mode 100644 index 00000000..588801e0 --- /dev/null +++ b/src/entities/ProfileEmployeeAbsentLateHistory.ts @@ -0,0 +1,17 @@ +import { Entity, Column } from "typeorm"; +import { ProfileEmployeeAbsentLate } from "./ProfileEmployeeAbsentLate"; +import { AbsentLateStatus, StampType } from "./ProfileAbsentLate"; + +@Entity("profileEmployeeAbsentLateHistory") +export class ProfileEmployeeAbsentLateHistory extends ProfileEmployeeAbsentLate { + @Column({ + nullable: true, + length: 40, + comment: "คีย์นอก(FK)ของตาราง ProfileEmployeeAbsentLate", + default: null, + }) + profileEmployeeAbsentLateId: string; +} + +// Export enums for re-use +export { AbsentLateStatus, StampType }; diff --git a/src/migration/1774408245407-add_table_absentLateHistory.ts b/src/migration/1774408245407-add_table_absentLateHistory.ts new file mode 100644 index 00000000..ff70745b --- /dev/null +++ b/src/migration/1774408245407-add_table_absentLateHistory.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddTableAbsentLateHistory1774408245407 implements MigrationInterface { + name = 'AddTableAbsentLateHistory1774408245407' + + public async up(queryRunner: QueryRunner): Promise { + + await queryRunner.query(`CREATE TABLE \`profileEmployeeAbsentLateHistory\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileEmployeeId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileEmployee', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, \`profileEmployeeAbsentLateId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileEmployeeAbsentLate', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`profileAbsentLateHistory\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง Profile', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, \`profileAbsentLateId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileAbsentLate', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLateHistory\` ADD CONSTRAINT \`FK_8b06ca79d6f75c7d6577c86f3d4\` FOREIGN KEY (\`profileEmployeeId\`) REFERENCES \`profileEmployee\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE \`profileAbsentLateHistory\` ADD CONSTRAINT \`FK_0fa6a843d0e6d901a4f2f56c541\` FOREIGN KEY (\`profileId\`) REFERENCES \`profile\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE \`profileAbsentLateHistory\``); + await queryRunner.query(`DROP TABLE \`profileEmployeeAbsentLateHistory\``); + } + +} From 936b28a9f421ab3ff1e67b9b3d0272f4c8933e1d Mon Sep 17 00:00:00 2001 From: Suphonchai Phoonsawat Date: Wed, 25 Mar 2026 14:33:44 +0700 Subject: [PATCH 162/178] Enhance OrganizationDotnetController: Add orgRevision to current_holders and improve profile mapping --- .../OrganizationDotnetController.ts | 101 ++++++++++++++++-- 1 file changed, 91 insertions(+), 10 deletions(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 5a84f3bc..4277a917 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -25,8 +25,6 @@ import { OrgRevision } from "../entities/OrgRevision"; import { OrgRoot } from "../entities/OrgRoot"; import { Position } from "../entities/Position"; import { PosMaster } from "../entities/PosMaster"; -import { PosMasterAssign } from "../entities/PosMasterAssign"; -import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; import { PosMasterHistory } from "../entities/PosMasterHistory"; import { Profile } from "../entities/Profile"; import { ProfileEducation } from "../entities/ProfileEducation"; @@ -221,6 +219,7 @@ export class OrganizationDotnetController extends Controller { .leftJoinAndSelect("profile.posType", "posType") .leftJoinAndSelect("profile.current_holders", "current_holders") .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") + .leftJoinAndSelect("current_holders.orgRevision", "orgRevision") .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") @@ -241,18 +240,59 @@ export class OrganizationDotnetController extends Controller { ) .andWhere(condition, conditionParams) .andWhere(selectedNodeCondition, selectedNodeConditionParams) + .select([ "profile.id", "profile.citizenId", "profile.prefix", "profile.firstName", "profile.lastName", + "current_holders", + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", ]) .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getManyAndCount(); - return new HttpSuccess({ data: profiles, total: total }); + const mapProfiles = profiles.map((profile) => ({ + id: profile.id, + citizenId: profile.citizenId, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + rootDnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgRoot?.ancestorDNA ?? null, + child1DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild1?.ancestorDNA ?? null, + child2DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild2?.ancestorDNA ?? null, + child3DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild3?.ancestorDNA ?? null, + child4DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild4?.ancestorDNA ?? null, + })); + + return new HttpSuccess({ data: mapProfiles, total: total }); } /** @@ -364,6 +404,7 @@ export class OrganizationDotnetController extends Controller { .leftJoinAndSelect("profile.profileSalary", "profileSalary") .leftJoinAndSelect("profile.current_holders", "current_holders") .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") + .leftJoinAndSelect("current_holders.orgRevision", "orgRevision") .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") @@ -389,12 +430,52 @@ export class OrganizationDotnetController extends Controller { "profile.prefix", "profile.firstName", "profile.lastName", + "current_holders", + "orgRevision", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", ]) .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getManyAndCount(); - return new HttpSuccess({ data: profileEmp, total: total }); + const mapProfiles = profileEmp.map((profile) => ({ + id: profile.id, + citizenId: profile.citizenId, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + rootDnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgRoot?.ancestorDNA ?? null, + child1DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild1?.ancestorDNA ?? null, + child2DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild2?.ancestorDNA ?? null, + child3DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild3?.ancestorDNA ?? null, + child4DnaId: + profile?.current_holders?.find( + (x) => + x.orgRevision?.id === findRevision.id && x.orgRevision?.orgRevisionIsDraft === false, + )?.orgChild4?.ancestorDNA ?? null, + })); + + return new HttpSuccess({ data: mapProfiles, total: total }); } /** @@ -1073,7 +1154,7 @@ export class OrganizationDotnetController extends Controller { profile.posLevel != null && (profile.posType.posTypeName == "บริหาร" || profile.posType.posTypeName == "อำนวยการ") ? `${profile.posType?.posTypeName ?? ""}${profile.posLevel?.posLevelName ?? ""}` - : (profile.posLevel?.posLevelName ?? null); + : profile.posLevel?.posLevelName ?? null; const _profileCurrent = profile?.current_holders?.find( (x) => x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, @@ -1409,7 +1490,7 @@ export class OrganizationDotnetController extends Controller { profile.posLevel && (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : (profile.posLevel?.posLevelName ?? null); + : profile.posLevel?.posLevelName ?? null; const mapProfile = { id: profile.id, @@ -1610,7 +1691,7 @@ export class OrganizationDotnetController extends Controller { profile.posLevel && (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : (profile.posLevel?.posLevelName ?? null); + : profile.posLevel?.posLevelName ?? null; /* ========================================= * 8. map response @@ -1868,7 +1949,7 @@ export class OrganizationDotnetController extends Controller { profile.posLevel && (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : (profile.posLevel?.posLevelName ?? null); + : profile.posLevel?.posLevelName ?? null; /* ========================================= * 8. map response @@ -2195,7 +2276,7 @@ export class OrganizationDotnetController extends Controller { profile.posLevel && (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : (profile.posLevel?.posLevelName ?? null); + : profile.posLevel?.posLevelName ?? null; /* ========================================= * 8. map response @@ -7542,7 +7623,7 @@ export class OrganizationDotnetController extends Controller { profile.posLevel && (profile.posType.posTypeName === "บริหาร" || profile.posType.posTypeName === "อำนวยการ") ? `${profile.posType.posTypeName}${profile.posLevel.posLevelName}` - : (profile.posLevel?.posLevelName ?? null); + : profile.posLevel?.posLevelName ?? null; /* ========================================= * position executive From effc7824602f024bd6f86a77d9e543d1d1de6d4b Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 25 Mar 2026 15:53:26 +0700 Subject: [PATCH 163/178] fix report kk1 --- src/controllers/ProfileController.ts | 213 ++++++++----- src/controllers/ProfileEmployeeController.ts | 306 ++++++++++++------- 2 files changed, 332 insertions(+), 187 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index ccec5b1a..7b883d73 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -87,6 +87,7 @@ import { OrgChild4 } from "../entities/OrgChild4"; import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; import { ProfileAssistance } from "../entities/ProfileAssistance"; import { CommandRecive } from "../entities/CommandRecive"; +import { CommandCode } from "../entities/CommandCode"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { CreatePosMasterHistoryOfficer, getTopDegrees } from "../services/PositionService"; import { ProfileLeaveService } from "../services/ProfileLeaveService"; @@ -146,6 +147,7 @@ export class ProfileController extends Controller { private permissionProflileRepository = AppDataSource.getRepository(PermissionProfile); private profileAssistanceRepository = AppDataSource.getRepository(ProfileAssistance); private commandReciveRepository = AppDataSource.getRepository(CommandRecive); + private commandCodeRepository = AppDataSource.getRepository(CommandCode); // Services private profileLeaveService = new ProfileLeaveService(); @@ -1370,26 +1372,26 @@ export class ProfileController extends Controller { yearData = { year }; for (let i = 1; i <= 11; i++) { - yearData[`leaveTypeCodeLv${i}`] = "-"; - yearData[`totalLeaveDaysLv${i}`] = "-"; - yearData[`leaveTypeNameLv${i}`] = "-"; + yearData[`leaveTypeCodeLv${i}`] = ""; + yearData[`totalLeaveDaysLv${i}`] = ""; + yearData[`leaveTypeNameLv${i}`] = ""; } leaves.push(yearData); } - yearData[leaveTypeCodeKey] = item.code ? item.code : "-"; + yearData[leaveTypeCodeKey] = item.code ? item.code : ""; yearData[totalLeaveDaysKey] = item.totalLeaveDays ? Extension.ToThaiNumber(item.totalLeaveDays.toString()) : "-"; - yearData[leaveTypeNameKey] = item.name ? item.name : "-"; + yearData[leaveTypeNameKey] = item.name ? item.name : ""; } } }); - if (leaves.length === 0) { - leaves.push({year:""}); - } + // if (leaves.length === 0) { + // leaves.push({year:""}); + // } // Query มาสาย/ขาดราชการ และ merge ตามปี const absentLate_raw = await this.profileAbsentLateRepo @@ -1417,9 +1419,9 @@ export class ProfileController extends Controller { if (!yearData) { yearData = { year }; for (let i = 1; i <= 11; i++) { - yearData[`leaveTypeCodeLv${i}`] = "-"; - yearData[`totalLeaveDaysLv${i}`] = "-"; - yearData[`leaveTypeNameLv${i}`] = "-"; + yearData[`leaveTypeCodeLv${i}`] = ""; + yearData[`totalLeaveDaysLv${i}`] = ""; + yearData[`leaveTypeNameLv${i}`] = ""; } leaves.push(yearData); } @@ -1428,18 +1430,18 @@ export class ProfileController extends Controller { if (item.status === "LATE") { yearData.late = item.totalAmount ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) - : "-"; + : ""; } else if (item.status === "ABSENT") { yearData.absent = item.totalAmount ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) - : "-"; + : ""; } }); - // เติมค่า "-" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ + // เติมค่า "" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ leaves.forEach((yearData) => { - if (!yearData.late) yearData.late = "-"; - if (!yearData.absent) yearData.absent = "-"; + if (!yearData.late) yearData.late = ""; + if (!yearData.absent) yearData.absent = ""; }); const leave2_raw = await this.profileLeaveRepository @@ -1568,39 +1570,78 @@ export class ProfileController extends Controller { ]; const position_raw = await this.salaryRepo.find({ - where: { + where: [{ profileId: id, commandCode: In(["0","1","2","3","4","8","9","10","11","12","13","14","15","16","20"]), // isEntry: false, }, + { profileId: id, commandCode: IsNull() }], order: { order: "ASC" }, }); + let _commandName:any = ""; + let _commandDateAffect:any = "" const positionList = position_raw.length > 0 - ? position_raw.map((item) => ({ - commandName: item.commandName ?? "", - commandDateAffect: item.commandDateAffect + ? await Promise.all(position_raw.map(async(item, idx, arr) => { + const isLast = idx === arr.length - 1; + if (isLast) { + _commandDateAffect = item.commandDateAffect ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - posNo: - item.posNoAbb && item.posNo - ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + : ""; + } + const _code = item.commandCode ? Number(item.commandCode) : null; + if (_code != null) { + _commandName = await this.commandCodeRepository.findOne({ + select: { name: true, code: true }, + where: { code: _code } + }); + } + const codeSitAbb = item.posNumCodeSitAbb ?? "-" + const commandNo = item.commandNo && item.commandYear + ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-" + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-" + return { + commandName: _commandName && _commandName.name + ? _commandName.name + : item.commandName, + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber( + Extension.ToThaiFullDate2(item.commandDateAffect) + ) : "", - position: item.positionName, - posType: item.positionType, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber( + Extension.ToThaiFullDate2(item.commandDateSign) + ) + : "", + posNo: + item.posNoAbb && item.posNo + ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + : "", + position: item.positionName, + posType: item.positionType, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee ? Extension.ToThaiNumber(item.positionCee) : null, - amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - positionSalaryAmount: item.positionSalaryAmount - ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) - : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + amount: item.amount + ? Extension.ToThaiNumber( + Number(item.amount).toLocaleString() + ) + : "", + positionSalaryAmount: item.positionSalaryAmount + ? Extension.ToThaiNumber( + Number(item.positionSalaryAmount).toLocaleString() + ) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}` + ), + }; })) : [ { @@ -1620,34 +1661,35 @@ export class ProfileController extends Controller { // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); - const retire_raw = await this.salaryRepo.findOne({ - where: { - profileId: id, - commandCode: In(["12", "15", "16"]), - }, - order: { order: "desc" }, - }); + // 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; + // if (retire_raw) { + // const startDate = retire_raw.commandDateAffect; - // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน - let daysCount = 0; - if (startDate) { - const start = new Date(startDate); - daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - } + // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน + // let daysCount = 0; + // if (startDate) { + // const start = new Date(startDate); + // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + // } - const startDateStr = startDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - : "-"; + // const startDateStr = startDate + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + // : "-"; - retires.push({ - date: `${startDateStr}`, - detail: retire_raw.commandName ?? "-", - day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - }); - } + // retires.push({ + // date: `${startDateStr}`, + // detail: retire_raw.commandName ?? "-", + // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + // }); + // } // กรณีไม่มีข้อมูล if (retires.length === 0) { @@ -1782,7 +1824,7 @@ export class ProfileController extends Controller { }); } } - // Merge รักษาการ และ ช่วยราชาร + // Merge รักษาการ และ ช่วยราชการ const actposition = [..._actpositions, ..._assistances]; const duty_raw = await this.dutyRepository.find({ @@ -1892,23 +1934,33 @@ export class ProfileController extends Controller { }); const otherIncome = otherIncome_raw.length > 0 - ? otherIncome_raw.map((item) => ({ - commandName: item.commandName ?? "", - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", - position: item.positionName, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + ? await Promise.all( + otherIncome_raw.map(async(item) => { + const codeSitAbb = item.posNumCodeSitAbb ?? "-" + const commandNo = item.commandNo && item.commandYear + ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-" + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-" + return { + commandName: item.commandName ?? "", + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : "", + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", + position: item.positionName, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", + refDoc: Extension.ToThaiNumber(`คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`) + } })) : [ { @@ -2042,8 +2094,7 @@ export class ProfileController extends Controller { appointDate: profiles?.dateAppoint ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(profiles.dateAppoint)) : "", - positionDate: - positionList.length > 0 ? positionList[positionList.length - 1].commandDateAffect : "", + positionDate: _commandDateAffect ?? "", citizenId: profiles.citizenId != null ? Extension.ToThaiNumber(profiles.citizenId.toString()) : "", fatherFullName: diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index c476e3ae..dee9c6d0 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -84,6 +84,7 @@ import { ProfileDuty } from "../entities/ProfileDuty"; import { getTopDegrees } from "../services/PositionService"; import { ProfileLeaveService } from "../services/ProfileLeaveService"; import { PostRetireToExprofile } from "./ExRetirementController"; +import { CommandCode } from "../entities/CommandCode"; @Route("api/v1/org/profile-employee") @Tags("ProfileEmployee") @Security("bearerAuth") @@ -136,6 +137,7 @@ export class ProfileEmployeeController extends Controller { private profileAssessmentsRepository = AppDataSource.getRepository(ProfileAssessment); private profileAbilityRepo = AppDataSource.getRepository(ProfileAbility); private profileAssistanceRepository = AppDataSource.getRepository(ProfileAssistance); + private commandCodeRepository = AppDataSource.getRepository(CommandCode); // Services private profileLeaveService = new ProfileLeaveService(); @@ -808,27 +810,68 @@ export class ProfileEmployeeController extends Controller { }, ]; - const leave_raw = await this.profileLeaveRepository.find({ - relations: { leaveType: true }, - where: { profileEmployeeId: id, isDeleted: false }, - order: { dateLeaveStart: "ASC" }, + const leave_raw = await this.profileLeaveRepository + .createQueryBuilder("profileLeave") + .leftJoinAndSelect("profileLeave.leaveType", "leaveType") + .select([ + "profileLeave.isDeleted", + "profileLeave.leaveTypeId", + "leaveType.name as name", + "leaveType.code as code", + "profileLeave.status", + "profileLeave.profileEmployeeId", + "MAX(profileLeave.dateLeaveStart) as maxDateLeaveStart", + ]) + .addSelect("SUM(profileLeave.leaveDays)", "totalLeaveDays") + .where("profileLeave.profileEmployeeId = :profileId", { profileId: id }) + .andWhere("profileLeave.isDeleted = :isDeleted", { isDeleted: false }) + .andWhere("profileLeave.status = :status", { status: "approve" }) + .groupBy("profileLeave.leaveTypeId") + .orderBy("code", "ASC") + .addOrderBy("maxDateLeaveStart", "ASC") + .getRawMany(); + + const leaves: any[] = []; + + leave_raw.forEach((item) => { + const leaveTypeCode = item.code ? item.code.trim().toUpperCase() : ""; + if (leaveTypeCode.startsWith("LV-")) { + const lvIndex = parseInt(leaveTypeCode.split("-")[1], 10); + + if (lvIndex >= 1 && lvIndex <= 11) { + const leaveTypeCodeKey = `leaveTypeCodeLv${lvIndex}`; + const totalLeaveDaysKey = `totalLeaveDaysLv${lvIndex}`; + const leaveTypeNameKey = `leaveTypeNameLv${lvIndex}`; + + const leaveDate = item.maxDateLeaveStart ? new Date(item.maxDateLeaveStart) : null; + const year = leaveDate + ? Extension.ToThaiNumber(Extension.ToThaiShortYear(leaveDate)) + : ""; + + let yearData = leaves.find((data) => data.year === year); + if (!yearData) { + yearData = { year }; + + for (let i = 1; i <= 11; i++) { + yearData[`leaveTypeCodeLv${i}`] = "-"; + yearData[`totalLeaveDaysLv${i}`] = "-"; + yearData[`leaveTypeNameLv${i}`] = "-"; + } + + leaves.push(yearData); + } + + yearData[leaveTypeCodeKey] = item.code ? item.code : "-"; + yearData[totalLeaveDaysKey] = item.totalLeaveDays + ? Extension.ToThaiNumber(item.totalLeaveDays.toString()) + : "-"; + yearData[leaveTypeNameKey] = item.name ? item.name : "-"; + } + } }); - const leaves = - leave_raw.length > 0 - ? leave_raw.map((item) => ({ - LeaveTypeName: item.leaveType.name, - DateLeaveStart: item.dateLeaveStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) - : "", - LeaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "", - })) - : [ - { - LeaveTypeName: "-", - DateLeaveStart: "-", - LeaveDays: "-", - }, - ]; + + // กรองเอา object ที่ไม่มี year ออก + const filteredLeaves = leaves.filter(item => item.year && item.year !== ""); const data = { fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, @@ -917,7 +960,7 @@ export class ProfileEmployeeController extends Controller { ? Extension.ToThaiNumber(Extension.ToThaiShortYear(profiles.profileAvatars[6].createdAt)) : null, insignias, - leaves, + leaves: filteredLeaves, certs, trainings, disciplines, @@ -943,6 +986,7 @@ export class ProfileEmployeeController extends Controller { public async getKk1new(@Path() id: string, @Request() req: RequestWithUser) { const profiles = await this.profileRepo.findOne({ relations: [ + "posType", "posLevel", "currentSubDistrict", "currentDistrict", @@ -1364,26 +1408,26 @@ export class ProfileEmployeeController extends Controller { yearData = { year }; for (let i = 1; i <= 11; i++) { - yearData[`leaveTypeCodeLv${i}`] = "-"; - yearData[`totalLeaveDaysLv${i}`] = "-"; - yearData[`leaveTypeNameLv${i}`] = "-"; + yearData[`leaveTypeCodeLv${i}`] = ""; + yearData[`totalLeaveDaysLv${i}`] = ""; + yearData[`leaveTypeNameLv${i}`] = ""; } leaves.push(yearData); } - yearData[leaveTypeCodeKey] = item.code ? item.code : "-"; + yearData[leaveTypeCodeKey] = item.code ? item.code : ""; yearData[totalLeaveDaysKey] = item.totalLeaveDays ? Extension.ToThaiNumber(item.totalLeaveDays.toString()) - : "-"; - yearData[leaveTypeNameKey] = item.name ? item.name : "-"; + : ""; + yearData[leaveTypeNameKey] = item.name ? item.name : ""; } } }); - if (leaves.length === 0) { - leaves.push({year:""}); - } + // if (leaves.length === 0) { + // leaves.push({year:""}); + // } // Query มาสาย/ขาดราชการ และ merge ตามปี const absentLate_raw = await this.profileEmployeeAbsentLateRepo @@ -1411,9 +1455,9 @@ export class ProfileEmployeeController extends Controller { if (!yearData) { yearData = { year }; for (let i = 1; i <= 11; i++) { - yearData[`leaveTypeCodeLv${i}`] = "-"; - yearData[`totalLeaveDaysLv${i}`] = "-"; - yearData[`leaveTypeNameLv${i}`] = "-"; + yearData[`leaveTypeCodeLv${i}`] = ""; + yearData[`totalLeaveDaysLv${i}`] = ""; + yearData[`leaveTypeNameLv${i}`] = ""; } leaves.push(yearData); } @@ -1422,18 +1466,18 @@ export class ProfileEmployeeController extends Controller { if (item.status === "LATE") { yearData.lateAmount = item.totalAmount ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) - : "-"; + : ""; } else if (item.status === "ABSENT") { yearData.absentAmount = item.totalAmount ? Extension.ToThaiNumber(parseFloat(item.totalAmount).toFixed(1)) - : "-"; + : ""; } }); - // เติมค่า "-" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ + // เติมค่า "" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ leaves.forEach((yearData) => { - if (!yearData.lateAmount) yearData.lateAmount = "-"; - if (!yearData.absentAmount) yearData.absentAmount = "-"; + if (!yearData.lateAmount) yearData.lateAmount = ""; + if (!yearData.absentAmount) yearData.absentAmount = ""; }); const leave2_raw = await this.profileLeaveRepository @@ -1562,39 +1606,78 @@ export class ProfileEmployeeController extends Controller { ]; const position_raw = await this.salaryRepo.find({ - where: { + where: [{ profileEmployeeId: id, commandCode: In(["0","1","2","3","4","8","9","10","11","12","13","14","15","16","20"]), // isEntry: false, }, + { profileEmployeeId: id, commandCode: IsNull() }], order: { order: "ASC" }, }); + let _commandName:any = ""; + let _commandDateAffect:any = "" const positionList = position_raw.length > 0 - ? position_raw.map((item) => ({ - commandName: item.commandName ?? "", - commandDateAffect: item.commandDateAffect + ? await Promise.all(position_raw.map(async(item, idx, arr) => { + const isLast = idx === arr.length - 1; + if (isLast) { + _commandDateAffect = item.commandDateAffect ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - posNo: - item.posNoAbb && item.posNo - ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + : ""; + } + const _code = item.commandCode ? Number(item.commandCode) : null; + if (_code != null) { + _commandName = await this.commandCodeRepository.findOne({ + select: { name: true, code: true }, + where: { code: _code } + }); + } + const codeSitAbb = item.posNumCodeSitAbb ?? "-" + const commandNo = item.commandNo && item.commandYear + ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-" + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-" + return { + commandName: _commandName && _commandName.name + ? _commandName.name + : item.commandName, + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber( + Extension.ToThaiFullDate2(item.commandDateAffect) + ) : "", - position: item.positionName, - posType: item.positionType, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber( + Extension.ToThaiFullDate2(item.commandDateSign) + ) + : "", + posNo: + item.posNoAbb && item.posNo + ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + : "", + position: item.positionName, + posType: item.positionType, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee ? Extension.ToThaiNumber(item.positionCee) : null, - amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - positionSalaryAmount: item.positionSalaryAmount - ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) - : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + amount: item.amount + ? Extension.ToThaiNumber( + Number(item.amount).toLocaleString() + ) + : "", + positionSalaryAmount: item.positionSalaryAmount + ? Extension.ToThaiNumber( + Number(item.positionSalaryAmount).toLocaleString() + ) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}` + ), + }; })) : [ { @@ -1610,6 +1693,7 @@ export class ProfileEmployeeController extends Controller { refDoc: "" }, ]; + // ลูกจ้างยังไม่มีรักษาการและช่วยราชการ // const actposition_raw = await this.profileActpositionRepo.find({ // select: ["dateStart", "dateEnd", "position", "isDeleted"], @@ -1789,23 +1873,33 @@ export class ProfileEmployeeController extends Controller { }); const otherIncome = otherIncome_raw.length > 0 - ? otherIncome_raw.map((item) => ({ - commandName: item.commandName ?? "", - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", - position: item.positionName, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${item.posNumCodeSitAbb ?? "-"} ที่ ${item.commandNo ?? "-"}/${item.commandYear>2500?item.commandYear:item.commandYear+543} ลว. ${(Extension.ToThaiFullDate2(item.commandDateAffect))}`) + ? await Promise.all( + otherIncome_raw.map(async(item) => { + const codeSitAbb = item.posNumCodeSitAbb ?? "-" + const commandNo = item.commandNo && item.commandYear + ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-" + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-" + return { + commandName: item.commandName ?? "", + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : "", + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", + position: item.positionName, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", + refDoc: Extension.ToThaiNumber(`คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`) + } })) : [ { @@ -1823,34 +1917,35 @@ export class ProfileEmployeeController extends Controller { // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); - const retire_raw = await this.salaryRepo.findOne({ - where: { - profileEmployeeId: id, - commandCode: In(["12", "15", "16"]), - }, - order: { order: "desc" }, - }); + // 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; + // if (retire_raw) { + // const startDate = retire_raw.commandDateAffect; - // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน - let daysCount = 0; - if (startDate) { - const start = new Date(startDate); - daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - } + // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน + // let daysCount = 0; + // if (startDate) { + // const start = new Date(startDate); + // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + // } - const startDateStr = startDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - : "-"; + // const startDateStr = startDate + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + // : "-"; - retires.push({ - date: `${startDateStr} - ปัจจุบัน`, - detail: retire_raw.commandName ?? "-", - day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" - }); - } + // retires.push({ + // date: `${startDateStr} - ปัจจุบัน`, + // detail: retire_raw.commandName ?? "-", + // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + // }); + // } // กรณีไม่มีข้อมูล if (retires.length === 0) { @@ -1863,11 +1958,11 @@ export class ProfileEmployeeController extends Controller { (_child2 == null ? "" : _child2 + " ") + (_child1 == null ? "" : _child1 + " ") + (_root == null ? "" : _root).trim() - const _position = profiles?.position != null ? - profiles?.posLevel != null - ? Extension.ToThaiNumber(`${profiles.position}${profiles.posLevel.posLevelName}`) + const _position = (profiles?.position != null ? + profiles.posType != null && profiles?.posLevel != null + ? Extension.ToThaiNumber(`${profiles.position} ${profiles.posType.posTypeShortName} ${profiles.posLevel.posLevelName}`) : profiles.position - : "" + : "").trim() const ocAssistance = await this.profileAssistanceRepository.findOne({ select: ["agency", "profileEmployeeId", "commandName", "status", "isDeleted"], where: { @@ -1960,8 +2055,7 @@ export class ProfileEmployeeController extends Controller { appointDate: profiles?.dateAppoint ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(profiles.dateAppoint)) : "", - positionDate: - positionList.length > 0 ? positionList[positionList.length - 1].commandDateAffect : "", + positionDate: _commandDateAffect ?? "", citizenId: profiles.citizenId != null ? Extension.ToThaiNumber(profiles.citizenId.toString()) : "", fatherFullName: From e01a4f22c5af7bd1d6f673b5da0808967f595e23 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 25 Mar 2026 17:55:02 +0700 Subject: [PATCH 164/178] fix report kk1 --- src/controllers/ProfileController.ts | 18 +++++++++--------- src/controllers/ProfileEmployeeController.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 7b883d73..b49581d0 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1389,10 +1389,6 @@ export class ProfileController extends Controller { } }); - // if (leaves.length === 0) { - // leaves.push({year:""}); - // } - // Query มาสาย/ขาดราชการ และ merge ตามปี const absentLate_raw = await this.profileAbsentLateRepo .createQueryBuilder("absentLate") @@ -1438,11 +1434,15 @@ export class ProfileController extends Controller { } }); - // เติมค่า "" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ - leaves.forEach((yearData) => { - if (!yearData.late) yearData.late = ""; - if (!yearData.absent) yearData.absent = ""; - }); + // // เติมค่า "" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ + // leaves.forEach((yearData) => { + // if (!yearData.late) yearData.late = ""; + // if (!yearData.absent) yearData.absent = ""; + // }); + + if (leaves.length === 0) { + leaves.push({year:""}); + } const leave2_raw = await this.profileLeaveRepository .createQueryBuilder("profileLeave") diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index dee9c6d0..a1ac02af 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -1425,10 +1425,6 @@ export class ProfileEmployeeController extends Controller { } }); - // if (leaves.length === 0) { - // leaves.push({year:""}); - // } - // Query มาสาย/ขาดราชการ และ merge ตามปี const absentLate_raw = await this.profileEmployeeAbsentLateRepo .createQueryBuilder("absentLate") @@ -1474,11 +1470,15 @@ export class ProfileEmployeeController extends Controller { } }); - // เติมค่า "" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ - leaves.forEach((yearData) => { - if (!yearData.lateAmount) yearData.lateAmount = ""; - if (!yearData.absentAmount) yearData.absentAmount = ""; - }); + // // เติมค่า "" ถ้าไม่มีข้อมูลมาสาย/ขาดราชการ + // leaves.forEach((yearData) => { + // if (!yearData.lateAmount) yearData.lateAmount = ""; + // if (!yearData.absentAmount) yearData.absentAmount = ""; + // }); + + if (leaves.length === 0) { + leaves.push({year:""}); + } const leave2_raw = await this.profileLeaveRepository .createQueryBuilder("profileLeave") From 264c13483875df93c64d8d56984b929a5c7ff777 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 30 Mar 2026 14:48:54 +0700 Subject: [PATCH 165/178] =?UTF-8?q?api=20list=20=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=E0=B8=8A=E0=B8=B7=E0=B9=88=E0=B8=AD=20(no=20token)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrganizationUnauthorizeController.ts | 327 +++++++++++++++++- 1 file changed, 326 insertions(+), 1 deletion(-) diff --git a/src/controllers/OrganizationUnauthorizeController.ts b/src/controllers/OrganizationUnauthorizeController.ts index deaa1f35..c1baff3b 100644 --- a/src/controllers/OrganizationUnauthorizeController.ts +++ b/src/controllers/OrganizationUnauthorizeController.ts @@ -4,7 +4,7 @@ import { AppDataSource } from "../database/data-source"; import HttpSuccess from "../interfaces/http-success"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; -import { Brackets, In, IsNull, MoreThanOrEqual, Not } from "typeorm"; +import { Brackets, In, IsNull, LessThanOrEqual, MoreThanOrEqual, Not } from "typeorm"; import { OrgRoot } from "../entities/OrgRoot"; import { PosMaster } from "../entities/PosMaster"; import { calculateRetireDate } from "../interfaces/utils"; @@ -24,6 +24,7 @@ import { viewEmployeePosMaster } from "../entities/view/viewEmployeePosMaster"; import { EmployeePosDict } from "../entities/EmployeePosDict"; import { ProfileSalary } from "../entities/ProfileSalary"; import { ProfileInsignia } from "../entities/ProfileInsignia"; +import { PosMasterHistory } from "../entities/PosMasterHistory"; @Route("api/v1/org/unauthorize") @Tags("OrganizationUnauthorize") @Response( @@ -44,6 +45,7 @@ export class OrganizationUnauthorizeController extends Controller { private posMasterRepository = AppDataSource.getRepository(PosMaster); private empPosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); private salaryRepo = AppDataSource.getRepository(ProfileSalary); + private posMasterHistoryRepository = AppDataSource.getRepository(PosMasterHistory); private insigniaRepo = AppDataSource.getRepository(ProfileInsignia); @Post("user/reset-password") async forgetPassword( @@ -3107,4 +3109,327 @@ export class OrganizationUnauthorizeController extends Controller { await this.profileEmpRepo.save(profiles); return new HttpSuccess(); } + + /** + * API รายชื่อข้าราชการ (unauthorize) + * + * @summary รายชื่อข้าราชการ + * + */ + @Post("officer-list") + async officerList( + @Body() + body: { + reqNode?: number; + reqNodeId?: string; + date: Date; + }, + ) { + let typeCondition: any = {}; + + // Build typeCondition based on reqNode and reqNodeId (similar to OWNER/ROOT/PARENT logic) + switch (body.reqNode) { + case 0: + typeCondition = { + rootDnaId: body.reqNodeId, + }; + break; + case 1: + typeCondition = { + child1DnaId: body.reqNodeId, + }; + break; + case 2: + typeCondition = { + child2DnaId: body.reqNodeId, + }; + break; + case 3: + typeCondition = { + child3DnaId: body.reqNodeId, + }; + break; + case 4: + typeCondition = { + child4DnaId: body.reqNodeId, + }; + break; + default: + typeCondition = {}; + break; + } + + const date = body.date ? new Date(body.date.toISOString().slice(0, 10)) : new Date(); + // set เวลาเป็น 23:59:59 ของวันนั้น + date.setHours(23, 59, 59, 999); + + let profile = await this.posMasterHistoryRepository.find({ + where: { + ...typeCondition, + createdAt: LessThanOrEqual(date), + }, + order: { + firstName: "ASC", + lastName: "ASC", + createdAt: "DESC", + }, + }); + + // group1: group by ancestorDNA แล้วเลือก create_at ล่าสุด + const grouped1 = new Map(); + for (const item of profile) { + const key = `${item.ancestorDNA}`; + if (!grouped1.has(key)) { + grouped1.set(key, item); + } else { + const exist = grouped1.get(key); + if (exist && item.createdAt > exist.createdAt) { + grouped1.set(key, item); + } + } + } + // group2: group by shortName-posMasterNo จากค่าที่ได้จาก group1 + const grouped2 = new Map(); + for (const item of Array.from(grouped1.values())) { + const key = `${item.shortName}-${item.posMasterNo}`; + if (!grouped2.has(key)) { + grouped2.set(key, item); + } else { + const exist = grouped2.get(key); + if (exist && item.createdAt > exist.createdAt) { + grouped2.set(key, item); + } + } + } + // group3: group by firstName-lastName จากค่าที่ได้จาก group2 + const grouped3 = new Map(); + for (const item of Array.from(grouped2.values())) { + const key = `${item.firstName}-${item.lastName}`; + if (!grouped3.has(key)) { + grouped3.set(key, item); + } else { + const exist = grouped3.get(key); + if (exist && item.createdAt > exist.createdAt) { + grouped3.set(key, item); + } + } + } + + const profile_ = await Promise.all( + Array.from(grouped3.values()) + .filter((x) => x.profileId != null) + .map(async (item: PosMasterHistory) => { + let profile = await this.profileRepo.findOne({ + where: { id: item.profileId }, + }); + + return { + id: item.profileId, + prefix: item.prefix, + firstName: item.firstName, + lastName: item.lastName, + citizenId: profile?.citizenId ?? null, + dateStart: profile?.dateStart ?? null, + dateAppoint: profile?.dateAppoint ?? null, + keycloak: profile?.keycloak ?? null, + posNo: `${item.shortName} ${item.posMasterNo}`, + position: item.position, + positionLevel: item.posLevel, + positionType: item.posType, + orgRootId: item.rootDnaId, + orgChild1Id: item.child1DnaId, + orgChild2Id: item.child2DnaId, + orgChild3Id: item.child3DnaId, + orgChild4Id: item.child4DnaId, + }; + }), + ); + + return new HttpSuccess( + (profile_ ?? []).sort((a, b) => a.posNo.localeCompare(b.posNo, undefined, { numeric: true })), + ); + } + + /** + * API รายชื่อพนักงาน (unauthorize) + * + * @summary รายชื่อพนักงาน + * + */ + @Post("employee-list") + async employeeList( + @Body() + body: { + reqNode?: number; + reqNodeId?: string; + startDate?: Date; + endDate?: Date; + revisionId?: string; + }, + ) { + let typeCondition: any = {}; + + // Build typeCondition based on reqNode and reqNodeId (similar to OWNER/ROOT/PARENT logic) + switch (body.reqNode) { + case 0: + typeCondition = { + orgRoot: { + id: body.reqNodeId, + }, + }; + break; + case 1: + typeCondition = { + orgChild1: { + id: body.reqNodeId, + }, + }; + break; + case 2: + typeCondition = { + orgChild2: { + id: body.reqNodeId, + }, + }; + break; + case 3: + typeCondition = { + orgChild3: { + id: body.reqNodeId, + }, + }; + break; + case 4: + typeCondition = { + orgChild4: { + id: body.reqNodeId, + }, + }; + break; + default: + typeCondition = {}; + break; + } + + let profile = await this.profileEmpRepo.find({ + where: { isLeave: false, isRetirement: false, current_holders: typeCondition }, + relations: [ + "posType", + "posLevel", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + ], + order: { + current_holders: { + orgRoot: { + orgRootOrder: "ASC", + }, + orgChild1: { + orgChild1Order: "ASC", + }, + orgChild2: { + orgChild2Order: "ASC", + }, + orgChild3: { + orgChild3Order: "ASC", + }, + orgChild4: { + orgChild4Order: "ASC", + }, + posMasterNo: "ASC", + }, + }, + }); + + let findRevision = await this.orgRevisionRepository.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + + if (body.revisionId) { + findRevision = await this.orgRevisionRepository.findOne({ + where: { id: body.revisionId }, + }); + } + + const profile_ = await Promise.all( + profile.map(async (item: ProfileEmployee) => { + const shortName = + item.current_holders.length == 0 + ? null + : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4 != + null + ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` + : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild3 != null + ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` + : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild2 != null + ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` + : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgChild1 != null + ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` + : item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) != + null && + item.current_holders.find((x) => x.orgRevisionId == findRevision?.id) + ?.orgRoot != null + ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision?.id)?.posMasterNo}` + : null; + const Oc = + item.current_holders.length == 0 + ? null + : item.current_holders[0].orgChild4 != null + ? `${item.current_holders[0].orgChild4.orgChild4Name}/${item.current_holders[0].orgChild3.orgChild3Name}/${item.current_holders[0].orgChild2.orgChild2Name}/${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}` + : item.current_holders[0].orgChild3 != null + ? `${item.current_holders[0].orgChild3.orgChild3Name}/${item.current_holders[0].orgChild2.orgChild2Name}/${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}` + : item.current_holders[0].orgChild2 != null + ? `${item.current_holders[0].orgChild2.orgChild2Name}/${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}` + : item.current_holders[0].orgChild1 != null + ? `${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}` + : item.current_holders[0].orgRoot != null + ? `${item.current_holders[0].orgRoot.orgRootName}` + : null; + + let _posMaster = await this.empPosMasterRepository.findOne({ + where: { + orgRevisionId: findRevision?.id, + current_holderId: item.id, + }, + }); + + return { + id: item.id, + prefix: item.prefix, + firstName: item.firstName, + lastName: item.lastName, + citizenId: item.citizenId, + dateStart: item.dateStart, + dateAppoint: item.dateAppoint, + keycloak: item.keycloak, + posNo: shortName, + position: item.position, + positionLevel: + item.posType?.posTypeShortName && item.posLevel?.posLevelName + ? `${item.posType?.posTypeShortName} ${item.posLevel?.posLevelName}` + : null, + positionType: item.posType?.posTypeName ?? null, + oc: Oc, + orgRootId: _posMaster?.orgRootId, + orgChild1Id: _posMaster?.orgChild1Id, + orgChild2Id: _posMaster?.orgChild2Id, + orgChild3Id: _posMaster?.orgChild3Id, + orgChild4Id: _posMaster?.orgChild4Id, + }; + }), + ); + + return new HttpSuccess(profile_); + } } From dc31ec0d7d8701b44972c0733d51ea748ca9936b Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 31 Mar 2026 10:48:49 +0700 Subject: [PATCH 166/178] =?UTF-8?q?api=20=E0=B8=AA=E0=B8=A3=E0=B9=89?= =?UTF-8?q?=E0=B8=B2=E0=B8=87=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=B9?= =?UTF-8?q?=E0=B8=A5=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=A1=E0=B8=B2=E0=B8=AA?= =?UTF-8?q?=E0=B8=B2=E0=B8=A2/=E0=B8=82=E0=B8=B2=E0=B8=94=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=8A=E0=B8=81=E0=B8=B2=E0=B8=A3=20(no=20token)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrganizationUnauthorizeController.ts | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/controllers/OrganizationUnauthorizeController.ts b/src/controllers/OrganizationUnauthorizeController.ts index c1baff3b..fa67daa0 100644 --- a/src/controllers/OrganizationUnauthorizeController.ts +++ b/src/controllers/OrganizationUnauthorizeController.ts @@ -25,6 +25,18 @@ import { EmployeePosDict } from "../entities/EmployeePosDict"; import { ProfileSalary } from "../entities/ProfileSalary"; import { ProfileInsignia } from "../entities/ProfileInsignia"; import { PosMasterHistory } from "../entities/PosMasterHistory"; +import { + ProfileAbsentLate, + CreateProfileAbsentLate, + CreateProfileAbsentLateBatch, +} from "../entities/ProfileAbsentLate"; +import { ProfileAbsentLateHistory } from "../entities/ProfileAbsentLateHistory"; +import { + ProfileEmployeeAbsentLate, + CreateProfileEmployeeAbsentLate, + CreateProfileEmployeeAbsentLateBatch, +} from "../entities/ProfileEmployeeAbsentLate"; +import { ProfileEmployeeAbsentLateHistory } from "../entities/ProfileEmployeeAbsentLateHistory"; @Route("api/v1/org/unauthorize") @Tags("OrganizationUnauthorize") @Response( @@ -47,6 +59,10 @@ export class OrganizationUnauthorizeController extends Controller { private salaryRepo = AppDataSource.getRepository(ProfileSalary); private posMasterHistoryRepository = AppDataSource.getRepository(PosMasterHistory); private insigniaRepo = AppDataSource.getRepository(ProfileInsignia); + private absentLateRepo = AppDataSource.getRepository(ProfileAbsentLate); + private absentLateHistoryRepo = AppDataSource.getRepository(ProfileAbsentLateHistory); + private empAbsentLateRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLate); + private empAbsentLateHistoryRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLateHistory); @Post("user/reset-password") async forgetPassword( @Body() @@ -3432,4 +3448,110 @@ export class OrganizationUnauthorizeController extends Controller { return new HttpSuccess(profile_); } + + /** + * API สร้างข้อมูลการมาสาย/ขาดราชการของข้าราชการ (สำหรับ schedule) + * @summary API สร้างข้อมูลการมาสาย/ขาดราชการของข้าราชการ (สำหรับ Job schedule) + */ + @Post("profile/absent-late/batch") + async newAbsentLateBatch(@Body() body: CreateProfileAbsentLateBatch) { + // กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย) + if (!body.records || body.records.length === 0) { + return new HttpSuccess({ count: 0, ids: [] }); + } + + const profileIds = [...new Set(body.records.map((r) => r.profileId))]; + const profiles = await this.profileRepo.findBy({ + id: In(profileIds), + }); + + const foundProfileIds = new Set(profiles.map((p) => p.id)); + const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileId)); + + // กรณีไม่พบ profile เลย + if (validRecords.length === 0) { + return new HttpSuccess({ count: 0, ids: [] }); + } + + const meta = { + createdUserId: "SYSTEM", + createdFullName: "SYSTEM", + lastUpdateUserId: "SYSTEM", + lastUpdateFullName: "SYSTEM", + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + const records = validRecords.map((item) => { + const data = new ProfileAbsentLate(); + Object.assign(data, { ...item, ...meta }); + return data; + }); + + const result = await this.absentLateRepo.save(records); + + // บันทึก history สำหรับแต่ละ record + const historyRecords = result.map((data) => { + const history = new ProfileAbsentLateHistory(); + Object.assign(history, { ...data, id: undefined }); + history.profileAbsentLateId = data.id; + return history; + }); + await this.absentLateHistoryRepo.save(historyRecords); + + return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) }); + } + + /** + * API สร้างข้อมูลการมาสาย/ขาดราชการของลูกจ้างประจำ (สำหรับ schedule) + * @summary API สร้างข้อมูลการมาสาย/ขาดราชการของลูกจ้างประจำ (สำหรับ schedule) + */ + @Post("profile-employee/absent-late/batch") + async newEmpAbsentLateBatch(@Body() body: CreateProfileEmployeeAbsentLateBatch) { + // กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย) + if (!body.records || body.records.length === 0) { + return new HttpSuccess({ count: 0, ids: [] }); + } + + const profileIds = [...new Set(body.records.map((r) => r.profileEmployeeId))]; + const profiles = await this.profileEmpRepo.findBy({ + id: In(profileIds), + }); + + const foundProfileIds = new Set(profiles.map((p) => p.id)); + const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileEmployeeId)); + + // กรณีไม่พบ profile เลย + if (validRecords.length === 0) { + return new HttpSuccess({ count: 0, ids: [] }); + } + + const meta = { + createdUserId: "SYSTEM", + createdFullName: "SYSTEM", + lastUpdateUserId: "SYSTEM", + lastUpdateFullName: "SYSTEM", + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + const records = validRecords.map((item) => { + const data = new ProfileEmployeeAbsentLate(); + Object.assign(data, { ...item, ...meta }); + return data; + }); + + const result = await this.empAbsentLateRepo.save(records); + + // บันทึก history สำหรับแต่ละ record + const historyRecords = result.map((data) => { + const history = new ProfileEmployeeAbsentLateHistory(); + Object.assign(history, { ...data, id: undefined }); + history.profileEmployeeAbsentLateId = data.id; + return history; + }); + await this.empAbsentLateHistoryRepo.save(historyRecords); + + return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) }); + } } From d553c1406c6520b19d64affae3b979d65c40bdee Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 1 Apr 2026 17:50:38 +0700 Subject: [PATCH 167/178] =?UTF-8?q?[Fix]=20=E0=B8=81=E0=B8=A3=E0=B8=93?= =?UTF-8?q?=E0=B8=B5=E0=B8=9E=E0=B9=89=E0=B8=99=E0=B8=A3=E0=B8=B2=E0=B8=8A?= =?UTF-8?q?=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B9=81=E0=B8=81=E0=B9=89=E0=B9=84?= =?UTF-8?q?=E0=B8=82=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=E0=B9=84=E0=B8=A1?= =?UTF-8?q?=E0=B9=88=E0=B8=95=E0=B9=89=E0=B8=AD=E0=B8=87=20clear=20?= =?UTF-8?q?=E0=B8=9F=E0=B8=B4=E0=B8=A5=E0=B8=94=E0=B9=8C=20keycloak=20#228?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 30 ++++++++++++------- src/controllers/ProfileController.ts | 9 +++--- src/controllers/ProfileEmployeeController.ts | 9 +++--- .../ProfileEmployeeTempController.ts | 15 +++++----- src/controllers/UserController.ts | 16 ++++++---- 5 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 016b4768..2993f480 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -1657,7 +1657,8 @@ export class CommandController extends Controller { // console.log("4. disable keycloak/authen") const delUserKeycloak = await deleteUser(_profile.keycloak, adminToken); if (delUserKeycloak) { - _profile.keycloak = ""; + // Task #228 + // _profile.keycloak = ""; _profile.roleKeycloaks = []; } } @@ -1713,7 +1714,8 @@ export class CommandController extends Controller { // disable keycloak/authen const delUserKeycloak = await deleteUser(_profileEmp.keycloak, adminToken); if (delUserKeycloak) { - _profileEmp.keycloak = ""; + // Task #228 + // _profileEmp.keycloak = ""; _profileEmp.roleKeycloaks = []; } } @@ -4128,7 +4130,8 @@ export class CommandController extends Controller { if (profile.keycloak != null) { const delUserKeycloak = await deleteUser(profile.keycloak); if (delUserKeycloak) { - profile.keycloak = _null; + // Task #228 + // profile.keycloak = _null; profile.roleKeycloaks = []; profile.isActive = false; } @@ -4532,7 +4535,8 @@ export class CommandController extends Controller { if (profile.keycloak != null) { const delUserKeycloak = await deleteUser(profile.keycloak); if (delUserKeycloak) { - profile.keycloak = _null; + // Task #228 + // profile.keycloak = _null; profile.roleKeycloaks = []; profile.isActive = false; } @@ -4752,7 +4756,8 @@ export class CommandController extends Controller { if (profile.keycloak != null) { const delUserKeycloak = await deleteUser(profile.keycloak); if (delUserKeycloak) { - profile.keycloak = _null; + // Task #228 + // profile.keycloak = _null; profile.roleKeycloaks = []; profile.isActive = false; } @@ -5248,7 +5253,8 @@ export class CommandController extends Controller { if (_profile.keycloak != null) { const delUserKeycloak = await deleteUser(_profile.keycloak); if (delUserKeycloak) { - _profile.keycloak = _null; + // Task #228 + // _profile.keycloak = _null; _profile.roleKeycloaks = []; _profile.isActive = false; } @@ -5431,7 +5437,8 @@ export class CommandController extends Controller { if (_profile.keycloak != null) { const delUserKeycloak = await deleteUser(_profile.keycloak); if (delUserKeycloak) { - _profile.keycloak = _null; + // Task #228 + // _profile.keycloak = _null; _profile.roleKeycloaks = []; _profile.isActive = false; } @@ -5769,7 +5776,8 @@ export class CommandController extends Controller { if (_profile.keycloak != null) { const delUserKeycloak = await deleteUser(_profile.keycloak); if (delUserKeycloak) { - _profile.keycloak = _null; + // Task #228 + // _profile.keycloak = _null; _profile.roleKeycloaks = []; _profile.isActive = false; } @@ -6206,7 +6214,8 @@ export class CommandController extends Controller { if (_profile.keycloak != null) { const delUserKeycloak = await deleteUser(_profile.keycloak); if (delUserKeycloak) { - _profile.keycloak = _null; + // Task #228 + // _profile.keycloak = _null; _profile.roleKeycloaks = []; _profile.isActive = false; } @@ -6503,7 +6512,8 @@ export class CommandController extends Controller { if (profileEmployee.keycloak != null) { // const delUserKeycloak = await deleteUser(profileEmployee.keycloak); // if (delUserKeycloak) { - profileEmployee.keycloak = _null; + // Task #228 + // profileEmployee.keycloak = _null; profileEmployee.roleKeycloaks = []; profileEmployee.isActive = false; // } diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index b49581d0..18e3f96c 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -11133,9 +11133,9 @@ export class ProfileController extends Controller { } /** - * API อัพเดทเกษียณ + * API อัพเดทถึงแก่กรรม * - * @summary อัพเดทเกษียณ (ADMIN) + * @summary อัพเดทถึงแก่กรรม (ADMIN) * * @param {string} id Id ทะเบียนประวัติ */ @@ -11263,7 +11263,8 @@ export class ProfileController extends Controller { if (profile.keycloak != null) { const delUserKeycloak = await deleteUser(profile.keycloak); if (delUserKeycloak) { - profile.keycloak = _null; + // Task #228 + // profile.keycloak = _null; profile.roleKeycloaks = []; profile.isActive = false; } @@ -11371,7 +11372,7 @@ export class ProfileController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.keycloak IS NULL") + .where("profile.isActive = :isActive", { isActive: false }) .andWhere( new Brackets((qb) => { qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` }); diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index a1ac02af..ffdb2085 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -5560,9 +5560,9 @@ export class ProfileEmployeeController extends Controller { } /** - * API อัพเดทเกษียณ + * API อัพเดทถึงแก่กรรม * - * @summary อัพเดทเกษียณ (ADMIN) + * @summary อัพเดทถึงแก่กรรม (ADMIN) * * @param {string} id Id ทะเบียนประวัติ */ @@ -5687,7 +5687,8 @@ export class ProfileEmployeeController extends Controller { if (profile.keycloak != null) { const delUserKeycloak = await deleteUser(profile.keycloak); if (delUserKeycloak) { - profile.keycloak = _null; + // Task #228 + // profile.keycloak = _null; profile.roleKeycloaks = []; profile.isActive = false; } @@ -6161,7 +6162,7 @@ export class ProfileEmployeeController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.keycloak IS NULL") + .where("profile.isActive = :isActive", { isActive: false }) .andWhere( new Brackets((qb) => { qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` }); diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index 7930b872..a864710d 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -3459,9 +3459,9 @@ export class ProfileEmployeeTempController extends Controller { } /** - * API อัพเดทเกษียณ + * API อัพเดทถึงแก่กรรม * - * @summary อัพเดทเกษียณ (ADMIN) + * @summary อัพเดทถึงแก่กรรม (ADMIN) * * @param {string} id Id ทะเบียนประวัติ */ @@ -3586,7 +3586,8 @@ export class ProfileEmployeeTempController extends Controller { if (profile.keycloak != null) { const delUserKeycloak = await deleteUser(profile.keycloak); if (delUserKeycloak) { - profile.keycloak = _null; + // Task #228 + // profile.keycloak = _null; profile.roleKeycloaks = []; profile.isActive = false; } @@ -3987,7 +3988,7 @@ export class ProfileEmployeeTempController extends Controller { case "citizenId": [findProfile, total] = await this.profileRepo.findAndCount({ where: { - keycloak: IsNull(), + isActive: false, citizenId: Like(`%${body.keyword}%`), }, relations: ["posType", "posLevel", "current_holders"], @@ -3999,7 +4000,7 @@ export class ProfileEmployeeTempController extends Controller { case "firstname": [findProfile, total] = await this.profileRepo.findAndCount({ where: { - keycloak: IsNull(), + isActive: false, firstName: Like(`%${body.keyword}%`), }, relations: ["posType", "posLevel", "current_holders"], @@ -4011,7 +4012,7 @@ export class ProfileEmployeeTempController extends Controller { case "lastname": [findProfile, total] = await this.profileRepo.findAndCount({ where: { - keycloak: IsNull(), + isActive: false, lastName: Like(`%${body.keyword}%`), }, relations: ["posType", "posLevel", "current_holders"], @@ -4023,7 +4024,7 @@ export class ProfileEmployeeTempController extends Controller { default: [findProfile, total] = await this.profileRepo.findAndCount({ where: { - keycloak: IsNull(), + isActive: false, }, relations: ["posType", "posLevel", "current_holders"], skip, diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 9889bd9b..52b97622 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -274,14 +274,16 @@ export class KeycloakController extends Controller { }); if (!profileEmp) { } else { - const _null: any = null; - profileEmp.keycloak = _null; + // Task #228 + // const _null: any = null; + // profileEmp.keycloak = _null; profileEmp.roleKeycloaks = []; await this.profileEmpRepo.save(profileEmp); } } else { - const _null: any = null; - profile.keycloak = _null; + // Task #228 + // const _null: any = null; + // profile.keycloak = _null; profile.roleKeycloaks = []; await this.profileRepo.save(profile); return new HttpSuccess(); @@ -566,7 +568,8 @@ export class KeycloakController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") + // .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") + .where("profile.isActive = :isActive", { isActive: true }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere( @@ -609,7 +612,8 @@ export class KeycloakController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") + // .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") + .where("profileEmployee.isActive = :isActive", { isActive: true }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere({ employeeClass: "PERM" }) From b69f8a6c085ab7cd0b8aa723c58f15759a4c9a64 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 2 Apr 2026 11:27:39 +0700 Subject: [PATCH 168/178] =?UTF-8?q?fix=20=E0=B8=AA=E0=B9=88=E0=B8=87?= =?UTF-8?q?=E0=B8=AD=E0=B8=AD=E0=B8=81=E0=B8=84=E0=B8=B3=E0=B8=AA=E0=B8=B1?= =?UTF-8?q?=E0=B9=88=E0=B8=87=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=8A=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=9C=E0=B8=B9=E0=B9=89=E0=B8=AA=E0=B8=AD?= =?UTF-8?q?=E0=B8=9A=E0=B8=9C=E0=B9=88=E0=B8=B2=E0=B8=99=20=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=A2=E0=B8=8A=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B9=84?= =?UTF-8?q?=E0=B8=A1=E0=B9=88=E0=B9=81=E0=B8=AA=E0=B8=94=E0=B8=87=E0=B9=83?= =?UTF-8?q?=E0=B8=99=E0=B9=82=E0=B8=84=E0=B8=A3=E0=B8=87=E0=B8=AA=E0=B8=A3?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=87=20#2402?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 37 +++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 2993f480..6e7cb637 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -6844,12 +6844,36 @@ export class CommandController extends Controller { } //Position if (item.bodyPosition && item.bodyPosition != null) { - const posMaster = await this.posMasterRepository.findOne({ - where: { id: item.bodyPosition.posmasterId }, + // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) + let posMaster = await this.posMasterRepository.findOne({ + where: { + id: item.bodyPosition.posmasterId, + }, + relations: { orgRevision: 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 } + }); + } + if (posMaster == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน const posMasterOld = await this.posMasterRepository.findOne({ where: { current_holderId: profile.id, @@ -6857,10 +6881,12 @@ export class CommandController extends Controller { }, }); if (posMasterOld != null) { + // เคลียร์คนครองเก่าออกจากตำแหน่งเดิม posMasterOld.current_holderId = null; posMasterOld.lastUpdatedAt = new Date(); } + // หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก const positionOld = await this.positionRepository.findOne({ where: { posMasterId: posMasterOld?.id, @@ -6872,9 +6898,10 @@ export class CommandController extends Controller { await this.positionRepository.save(positionOld); } + // STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่ const checkPosition = await this.positionRepository.find({ where: { - posMasterId: item.bodyPosition.posmasterId, + posMasterId: posMaster.id, positionIsSelected: true, }, }); @@ -6886,6 +6913,7 @@ export class CommandController extends Controller { await this.positionRepository.save(clearPosition); } + // STEP 4: กำหนดคนครองใหม่ให้กับ posMaster posMaster.current_holderId = profile.id; posMaster.lastUpdatedAt = new Date(); // posMaster.conditionReason = _null; @@ -6896,10 +6924,11 @@ export class CommandController extends Controller { } await this.posMasterRepository.save(posMaster); + // STEP 5: กำหนด position ใหม่ const positionNew = await this.positionRepository.findOne({ where: { id: item.bodyPosition.positionId, - posMasterId: item.bodyPosition.posmasterId, + posMasterId: posMaster.id, // ใช้ id ของ posMaster ตัวใหม่ }, }); if (positionNew != null) { From 38e5ed0e916319d2cded078fff32f33e2f04b977 Mon Sep 17 00:00:00 2001 From: adisak Date: Thu, 2 Apr 2026 12:04:33 +0700 Subject: [PATCH 169/178] =?UTF-8?q?#2387=20[=E0=B8=81=E0=B8=97=E0=B8=A1].?= =?UTF-8?q?=20=E0=B8=A3=E0=B8=B0=E0=B8=9A=E0=B8=9A=E0=B9=82=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=87=E0=B8=AA=E0=B8=A3=E0=B9=89=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=AD=E0=B8=B1=E0=B8=95=E0=B8=A3=E0=B8=B2=E0=B8=81=E0=B8=B3?= =?UTF-8?q?=E0=B8=A5=E0=B8=B1=E0=B8=87=20>>=20=E0=B8=81=E0=B8=A3=E0=B8=93?= =?UTF-8?q?=E0=B8=B5=E0=B8=99=E0=B8=B1=E0=B9=88=E0=B8=87=E0=B8=97=E0=B8=B1?= =?UTF-8?q?=E0=B8=9A=E0=B8=95=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88?= =?UTF-8?q?=E0=B8=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 24 ++++++++++++++--------- src/controllers/OrganizationController.ts | 9 +++++---- src/controllers/PositionController.ts | 13 +++++++----- src/services/rabbitmq.ts | 3 ++- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 016b4768..29f6b19c 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -3699,11 +3699,14 @@ export class CommandController extends Controller { posMasterId: item.posmasterId, }, }); + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; + if(!posMaster.isSit){ + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + } profile.amount = item.amount ?? null; profile.amountSpecial = item.amountSpecial ?? null; await this.profileRepository.save(profile); @@ -6892,14 +6895,17 @@ export class CommandController extends Controller { posMasterId: item.bodyPosition.posmasterId, }, }); + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; - // profile.dateStart = new Date(); - await this.profileRepository.save(profile, { data: req }); - setLogDataDiff(req, { before, after: profile }); + if(!posMaster.isSit){ + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + // profile.dateStart = new Date(); + 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/OrganizationController.ts b/src/controllers/OrganizationController.ts index e1767a43..e5a39411 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8927,16 +8927,17 @@ export class OrganizationController extends Controller { }); } + // Collect history data for the selected position + const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any; + // Collect profile update for the selected position - if (nextHolderId != null && draftPos.positionIsSelected) { + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (nextHolderId != null && draftPos.positionIsSelected && !draftPosMaster?.isSit) { profileUpdates.set(nextHolderId, { position: draftPos.positionName, posTypeId: draftPos.posTypeId, posLevelId: draftPos.posLevelId, }); - - // Collect history data for the selected position - const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any; if (draftPosMaster && draftPosMaster.ancestorDNA) { // Find the selected position from draft positions const selectedPos = diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index 3fff25c4..e3494349 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -3826,11 +3826,14 @@ export class PositionController extends Controller { where: { id: requestBody.position, posMasterId: requestBody.posMaster }, }); if (_position) { - _profile.position = _position.positionName; - _profile.posTypeId = _position.posTypeId; - _profile.posLevelId = _position.posLevelId; - await this.profileRepository.save(_profile); - setLogDataDiff(request, { before, after: _profile }); + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + 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 }); + } } } dataMaster.current_holderId = requestBody.profileId; diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 784f3873..a8011900 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -651,7 +651,8 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { await posMasterAssignRepository.save(newAssigns); } - if (item.next_holderId != null) { + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (item.next_holderId != null && !item.isSit) { const profile = await repoProfile.findOne({ where: { id: item.next_holderId == null ? "" : item.next_holderId }, }); From a40d98c5a9ec5ccefc83449827f02d98084596d7 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 2 Apr 2026 13:52:39 +0700 Subject: [PATCH 170/178] Migrate update profile &profileEmployee add isDelete --- src/entities/Profile.ts | 6 +++++ src/entities/ProfileEmployee.ts | 6 +++++ ..._and_profileemployee_add_field_isdelete.ts | 23 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/migration/1775112029663-update_profile_and_profileemployee_add_field_isdelete.ts diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index fe2f55d6..72a1d505 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -188,6 +188,12 @@ export class Profile extends EntityBase { }) keycloak: string; + @Column({ + comment: "สถานะการถูกลบผู้ใช้งานใน keycloak", + default: false, + }) + isDelete: boolean; + @Column({ comment: "ทดลองปฏิบัติหน้าที่", default: false, diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index 2c3f06e4..11c2159b 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -204,6 +204,12 @@ export class ProfileEmployee extends EntityBase { }) keycloak: string; + @Column({ + comment: "สถานะการถูกลบผู้ใช้งานใน keycloak", + default: false, + }) + isDelete: boolean; + @Column({ comment: "ทดลองปฏิบัติหน้าที่", default: false, diff --git a/src/migration/1775112029663-update_profile_and_profileemployee_add_field_isdelete.ts b/src/migration/1775112029663-update_profile_and_profileemployee_add_field_isdelete.ts new file mode 100644 index 00000000..706f3d59 --- /dev/null +++ b/src/migration/1775112029663-update_profile_and_profileemployee_add_field_isdelete.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateProfileAndProfileemployeeAddFieldIsdelete1775112029663 implements MigrationInterface { + name = 'UpdateProfileAndProfileemployeeAddFieldIsdelete1775112029663' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileEmployee\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileEmployeeHistory\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profile\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`); + + // Update ข้อมูลเดิม: ถ้า keycloak null → isDelete = true (1), ถ้ามีค่า → isDelete = false (0) + await queryRunner.query(`UPDATE \`profileEmployee\` SET \`isDelete\` = CASE WHEN \`keycloak\` IS NULL THEN 1 ELSE 0 END`); + await queryRunner.query(`UPDATE \`profile\` SET \`isDelete\` = CASE WHEN \`keycloak\` IS NULL THEN 1 ELSE 0 END`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`isDelete\``); + await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`isDelete\``); + await queryRunner.query(`ALTER TABLE \`profileEmployeeHistory\` DROP COLUMN \`isDelete\``); + await queryRunner.query(`ALTER TABLE \`profileEmployee\` DROP COLUMN \`isDelete\``); + } +} From da6fd7a39665d99a09f5d18b353b16f50921deb3 Mon Sep 17 00:00:00 2001 From: adisak Date: Thu, 2 Apr 2026 14:18:08 +0700 Subject: [PATCH 171/178] #2387 update --- src/controllers/PositionController.ts | 3 ++- src/controllers/ProfileGovernmentController.ts | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index e3494349..75d6c2a0 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -1451,7 +1451,8 @@ export class PositionController extends Controller { }), ); - if (posMaster.orgRevision?.orgRevisionIsCurrent == true) { + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (posMaster.orgRevision?.orgRevisionIsCurrent == true && !posMaster.isSit) { const _position = requestBody.positions.find((p) => p.positionIsSelected == true); if (_position) { const current_holderId: any = posMaster.current_holderId; diff --git a/src/controllers/ProfileGovernmentController.ts b/src/controllers/ProfileGovernmentController.ts index dcb138df..b3238672 100644 --- a/src/controllers/ProfileGovernmentController.ts +++ b/src/controllers/ProfileGovernmentController.ts @@ -287,9 +287,10 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); + //posMaster?.isSit แก้ไขชั่วคราว const data = { org: record?.isLeave == false ? org : orgLeave, //สังกัด - positionField: position == null ? null : position.positionField, //สายงาน + positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: @@ -302,11 +303,11 @@ export class ProfileGovernmentHistoryController extends Controller { : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท posExecutive: - position == null || position.posExecutive == null + position == null || position.posExecutive == null || posMaster?.isSit ? null : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null ? null : position.positionExecutiveField, //ด้านทางการบริหาร + positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา + positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), @@ -461,9 +462,10 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); + //posMaster?.isSit แก้ไขชั่วคราว const data = { org: record?.isLeave == false ? org : orgLeave, //สังกัด - positionField: position == null ? null : position.positionField, //สายงาน + positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: @@ -476,11 +478,11 @@ export class ProfileGovernmentHistoryController extends Controller { : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท posExecutive: - position == null || position.posExecutive == null + position == null || position.posExecutive == null || posMaster?.isSit ? null : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null ? null : position.positionExecutiveField, //ด้านทางการบริหาร + positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา + positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), From 212360a764a45415695f151f828433df75215a4b Mon Sep 17 00:00:00 2001 From: adisak Date: Thu, 2 Apr 2026 15:01:38 +0700 Subject: [PATCH 172/178] #2387 update --- src/controllers/ProfileGovernmentController.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controllers/ProfileGovernmentController.ts b/src/controllers/ProfileGovernmentController.ts index b3238672..8caaff28 100644 --- a/src/controllers/ProfileGovernmentController.ts +++ b/src/controllers/ProfileGovernmentController.ts @@ -110,19 +110,20 @@ export class ProfileGovernmentHistoryController extends Controller { orgShortName = posMaster.orgChild4?.orgChild4ShortName; } } + //posMaster?.isSit แก้ไขชั่วคราว const data = { org: org, //สังกัด - positionField: position == null ? null : position.positionField, //สายงาน + positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน position: record.position, //ตำแหน่ง posLevel: record.posLevel == null ? null : record.posLevel.posLevelName, //ระดับ posMasterNo: posMaster == null ? null : `${orgShortName} ${posMaster.posMasterNo}`, //เลขที่ตำแหน่ง posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท posExecutive: - position == null || position.posExecutive == null + position == null || position.posExecutive == null || posMaster?.isSit ? null : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร - positionArea: position == null ? null : position.positionArea, //ด้าน/สาขา - positionExecutiveField: position == null ? null : position.positionExecutiveField, //ด้านทางการบริหาร + positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา + positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate), dateRetireLaw: record.dateRetireLaw ?? null, // govAge: record.dateStart == null ? null : calculateAge(record.dateStart), From fb4196cfa23b3a79fe26e03c119cbbc934dc5b7b Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 2 Apr 2026 16:32:29 +0700 Subject: [PATCH 173/178] fix keycloak user & update isDelete --- src/controllers/CommandController.ts | 27 +- src/controllers/OrganizationController.ts | 10 +- .../ProfileChangeNameController.ts | 4 +- .../ProfileChangeNameEmployeeController.ts | 2 +- ...ProfileChangeNameEmployeeTempController.ts | 2 +- src/controllers/ProfileController.ts | 378 ++++++++++-------- src/controllers/ProfileEmployeeController.ts | 367 +++++++++-------- .../ProfileEmployeeTempController.ts | 3 +- src/controllers/UserController.ts | 6 +- 9 files changed, 445 insertions(+), 354 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 63cc937b..4fe95e78 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -1653,12 +1653,13 @@ export class CommandController extends Controller { _profile.leaveDate = _Date; _profile.dateLeave = _Date; _profile.lastUpdatedAt = _Date; - if (_profile.keycloak != null && _profile.keycloak != "") { + if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { // console.log("4. disable keycloak/authen") const delUserKeycloak = await deleteUser(_profile.keycloak, adminToken); if (delUserKeycloak) { // Task #228 // _profile.keycloak = ""; + _profile.isDelete = true; _profile.roleKeycloaks = []; } } @@ -1710,12 +1711,13 @@ export class CommandController extends Controller { _profileEmp.leaveDate = _Date; _profileEmp.dateLeave = _Date; _profileEmp.lastUpdatedAt = _Date; - if (_profileEmp.keycloak != null && _profileEmp.keycloak != "") { + if (_profileEmp.keycloak != null && _profileEmp.keycloak != "" && _profileEmp.isDelete === false) { // disable keycloak/authen const delUserKeycloak = await deleteUser(_profileEmp.keycloak, adminToken); if (delUserKeycloak) { // Task #228 // _profileEmp.keycloak = ""; + _profileEmp.isDelete = true; _profileEmp.roleKeycloaks = []; } } @@ -4130,13 +4132,14 @@ export class CommandController extends Controller { await removeProfileInOrganize(profile.id, "OFFICER"); } if (clearProfile.status) { - if (profile.keycloak != null) { + 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; @@ -4535,13 +4538,14 @@ export class CommandController extends Controller { } if (clearProfile.status) { - if (profile.keycloak != null) { + 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; @@ -4756,13 +4760,14 @@ export class CommandController extends Controller { const clearProfile = await checkCommandType(String(item.commandId)); const _null: any = null; if (clearProfile.status) { - if (profile.keycloak != null) { + 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.isLeave = item.isLeave; @@ -5253,13 +5258,14 @@ export class CommandController extends Controller { const clearProfile = await checkCommandType(String(item.commandId)); if (clearProfile.status) { retireTypeName = clearProfile.retireTypeName ?? ""; - if (_profile.keycloak != null) { + 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; @@ -5437,13 +5443,14 @@ export class CommandController extends Controller { const clearProfile = await checkCommandType(String(item.commandId)); if (clearProfile.status) { retireTypeName = clearProfile.retireTypeName ?? ""; - if (_profile.keycloak != null) { + 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; @@ -5776,13 +5783,14 @@ export class CommandController extends Controller { } const clearProfile = await checkCommandType(String(item.commandId)); if (clearProfile.status) { - if (_profile.keycloak != null) { + 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; @@ -6214,13 +6222,14 @@ export class CommandController extends Controller { const clearProfile = await checkCommandType(String(item.commandId)); const _null: any = null; if (clearProfile.status) { - if (_profile.keycloak != null) { + 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; diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index e5a39411..39752b7e 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7796,10 +7796,11 @@ export class OrganizationController extends Controller { profile.leaveType = "RETIRE"; profile.isActive = false; - if (profile.keycloak != null && profile.keycloak != "") { + if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { const delUserKeycloak = await deleteUser(profile.keycloak, token); if (delUserKeycloak) { - profile.keycloak = ""; + // profile.keycloak = ""; + profile.isDelete = true; profile.roleKeycloaks = []; checkOfficer += 1; } else { @@ -7824,10 +7825,11 @@ export class OrganizationController extends Controller { profileEmp.lastUpdatedAt = new Date(); profileEmp.isActive = false; - if (profileEmp.keycloak != null && profileEmp.keycloak != "") { + if (profileEmp.keycloak != null && profileEmp.keycloak != "" && profileEmp.isDelete === false) { const delUserKeycloak = await deleteUser(profileEmp.keycloak, token); if (delUserKeycloak) { - profileEmp.keycloak = ""; + // profileEmp.keycloak = ""; + profileEmp.isDelete = true; profileEmp.roleKeycloaks = []; checkEmployee += 1; } else { diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 77e63aa6..77cff634 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -115,7 +115,7 @@ export class ProfileChangeNameController extends Controller { await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); - if (profile != null && profile.keycloak != null) { + if (profile != null && profile.keycloak != null && profile.isDelete === false) { const result = await updateName( profile.keycloak, profile.firstName, @@ -186,7 +186,7 @@ export class ProfileChangeNameController extends Controller { } // ปิดไว้ก่อนเพราะ error ต้องใช้ keycloak ที่มีสิทธิ์ในการ update //update 17/07 - if (profile != null && profile.keycloak != null) { + if (profile != null && profile.keycloak != null && profile.isDelete === false) { const result = await updateName( profile.keycloak, profile.firstName, diff --git a/src/controllers/ProfileChangeNameEmployeeController.ts b/src/controllers/ProfileChangeNameEmployeeController.ts index 76f5da7f..c2df9c8c 100644 --- a/src/controllers/ProfileChangeNameEmployeeController.ts +++ b/src/controllers/ProfileChangeNameEmployeeController.ts @@ -121,7 +121,7 @@ export class ProfileChangeNameEmployeeController extends Controller { await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); - if (profile != null && profile.keycloak != null) { + if (profile != null && profile.keycloak != null && profile.isDelete === false) { const result = await updateName( profile.keycloak, profile.firstName, diff --git a/src/controllers/ProfileChangeNameEmployeeTempController.ts b/src/controllers/ProfileChangeNameEmployeeTempController.ts index 78aaa09d..92e1e2f3 100644 --- a/src/controllers/ProfileChangeNameEmployeeTempController.ts +++ b/src/controllers/ProfileChangeNameEmployeeTempController.ts @@ -113,7 +113,7 @@ export class ProfileChangeNameEmployeeTempController extends Controller { await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); - if (profile != null && profile.keycloak != null) { + if (profile != null && profile.keycloak != null && profile.isDelete === false) { const result = await updateName( profile.keycloak, profile.firstName, diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 18e3f96c..9e758e67 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -1037,7 +1037,7 @@ export class ProfileController extends Controller { profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.id; - + const root = profiles.current_holders == null || profiles.current_holders.length == 0 || @@ -1098,7 +1098,9 @@ export class ProfileController extends Controller { certificateType: item.certificateType ?? null, issuer: item.issuer ?? null, certificateNo: item.certificateNo ? Extension.ToThaiNumber(item.certificateNo) : null, - detail: Extension.ToThaiNumber(`${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim()), + detail: Extension.ToThaiNumber( + `${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim(), + ), issueToExpireDate: item.issueDate ? item.expireDate ? Extension.ToThaiNumber( @@ -1130,7 +1132,9 @@ export class ProfileController extends Controller { degree: item.name ? Extension.ToThaiNumber(item.name) : "", place: item.place ? Extension.ToThaiNumber(item.place) : "", duration: item.duration ? Extension.ToThaiNumber(item.duration) : "", - date: Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`) + date: Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`, + ), })) : [ { @@ -1138,7 +1142,7 @@ export class ProfileController extends Controller { degree: "", place: "", duration: "", - date: "" + date: "", }, ]; @@ -1181,14 +1185,14 @@ export class ProfileController extends Controller { ? `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)) : ""}` : `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.startDate))) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.endDate))) : ""}`, degree: `${item.degree ?? ""} ${item.field ?? ""}`.trim(), - level: item.educationLevel + level: item.educationLevel, })) : [ { institute: "", date: "", degree: "", - level: "" + level: "", }, ]; const salary_raw = await this.salaryRepo.find({ @@ -1308,7 +1312,9 @@ export class ProfileController extends Controller { section: item.section ? Extension.ToThaiNumber(item.section) : "", page: item.page ? Extension.ToThaiNumber(item.page) : "", refCommandDate: item.refCommandDate - ? Extension.ToThaiNumber(`ราชกิจจานุเบกษา เล่มที่ ${item.volume ?? "-"} ตอนที่ ${item.section ?? "-"} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`) + ? Extension.ToThaiNumber( + `ราชกิจจานุเบกษา เล่มที่ ${item.volume ?? "-"} ตอนที่ ${item.section ?? "-"} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`, + ) : "-", note: item.note ? Extension.ToThaiNumber(item.note) : "", })) @@ -1325,7 +1331,7 @@ export class ProfileController extends Controller { section: "", page: "", refCommandDate: "", - note: "" + note: "", }, ]; @@ -1405,9 +1411,7 @@ export class ProfileController extends Controller { // Merge มาสาย/ขาดราชการเข้า leaves array absentLate_raw.forEach((item) => { - const year = item.year - ? Extension.ToThaiNumber((item.year + 543).toString()) - : ""; + const year = item.year ? Extension.ToThaiNumber((item.year + 543).toString()) : ""; let yearData = leaves.find((data) => data.year === year); @@ -1441,7 +1445,7 @@ export class ProfileController extends Controller { // }); if (leaves.length === 0) { - leaves.push({year:""}); + leaves.push({ year: "" }); } const leave2_raw = await this.profileLeaveRepository @@ -1473,12 +1477,12 @@ export class ProfileController extends Controller { const displayType = leaveTypeCode === "LV-008" && item.leaveSubTypeName ? item.leaveSubTypeName - : (item.name || "-"); + : item.name || "-"; // ข้อที่ 2: แสดง reason ก่อนเสมอ ถ้ามี coupleDayLevelCountry ค่อยแสดงประเทศ const displayReason = item.coupleDayLevelCountry ? `${item.reason || ""} ลาไปประเทศ ${item.coupleDayLevelCountry}`.trim() - : (item.reason || "-"); + : item.reason || "-"; return { date: @@ -1570,79 +1574,93 @@ export class ProfileController extends Controller { ]; const position_raw = await this.salaryRepo.find({ - where: [{ - profileId: id, - commandCode: In(["0","1","2","3","4","8","9","10","11","12","13","14","15","16","20"]), - // isEntry: false, - }, - { profileId: id, commandCode: IsNull() }], + where: [ + { + profileId: id, + commandCode: In([ + "0", + "1", + "2", + "3", + "4", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "20", + ]), + // isEntry: false, + }, + { profileId: id, commandCode: IsNull() }, + ], order: { order: "ASC" }, }); - let _commandName:any = ""; - let _commandDateAffect:any = "" + let _commandName: any = ""; + let _commandDateAffect: any = ""; const positionList = position_raw.length > 0 - ? await Promise.all(position_raw.map(async(item, idx, arr) => { - const isLast = idx === arr.length - 1; - if (isLast) { - _commandDateAffect = item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : ""; - } - const _code = item.commandCode ? Number(item.commandCode) : null; - if (_code != null) { - _commandName = await this.commandCodeRepository.findOne({ - select: { name: true, code: true }, - where: { code: _code } - }); - } - const codeSitAbb = item.posNumCodeSitAbb ?? "-" - const commandNo = item.commandNo && item.commandYear - ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) - : "-" - const dateAffect = item.commandDateAffect - ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` - : "-" - return { - commandName: _commandName && _commandName.name - ? _commandName.name - : item.commandName, - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber( - Extension.ToThaiFullDate2(item.commandDateAffect) - ) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber( - Extension.ToThaiFullDate2(item.commandDateSign) - ) - : "", - posNo: - item.posNoAbb && item.posNo - ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + ? await Promise.all( + position_raw.map(async (item, idx, arr) => { + const isLast = idx === arr.length - 1; + if (isLast) { + _commandDateAffect = item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : ""; + } + const _code = item.commandCode ? Number(item.commandCode) : null; + if (_code != null) { + _commandName = await this.commandCodeRepository.findOne({ + select: { name: true, code: true }, + where: { code: _code }, + }); + } + const codeSitAbb = item.posNumCodeSitAbb ?? "-"; + const commandNo = + item.commandNo && item.commandYear + ? item.commandNo + + "/" + + (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-"; + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-"; + return { + commandName: + _commandName && _commandName.name ? _commandName.name : item.commandName, + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) : "", - position: item.positionName, - posType: item.positionType, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount - ? Extension.ToThaiNumber( - Number(item.amount).toLocaleString() - ) - : "", - positionSalaryAmount: item.positionSalaryAmount - ? Extension.ToThaiNumber( - Number(item.positionSalaryAmount).toLocaleString() - ) - : "", - refDoc: Extension.ToThaiNumber( - `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}` - ), - }; - })) + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + posNo: + item.posNoAbb && item.posNo + ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + : "", + position: item.positionName, + posType: item.positionType, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount + ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) + : "", + positionSalaryAmount: item.positionSalaryAmount + ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, + ), + }; + }), + ) : [ { commandName: "", @@ -1654,7 +1672,7 @@ export class ProfileController extends Controller { posLevel: "", amount: "", positionSalaryAmount: "", - refDoc: "" + refDoc: "", }, ]; @@ -1672,7 +1690,7 @@ export class ProfileController extends Controller { // if (retire_raw) { // const startDate = retire_raw.commandDateAffect; - + // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน // let daysCount = 0; // if (startDate) { @@ -1701,56 +1719,53 @@ export class ProfileController extends Controller { select: ["dateStart", "profileId", "dateEnd", "position", "commandId", "isDeleted"], where: { profileId: id, status: true, isDeleted: false }, order: { createdAt: "ASC" }, - }); + }); let _actpositions = []; - if (actposition_raw.length > 0){ + if (actposition_raw.length > 0) { for (const item of actposition_raw) { let _commandTypename: string = "รักษาการในตำแหน่ง"; let _document: string = "-"; let _org: string = "-"; if (item.commandId) { - const { - posNumCodeSitAbb, - commandTypeName, - commandNo, - commandYear, - commandExcecuteDate, - } = await getPosNumCodeSit(item.commandId, "REPORTED"); + const { posNumCodeSitAbb, commandTypeName, commandNo, commandYear, commandExcecuteDate } = + await getPosNumCodeSit(item.commandId, "REPORTED"); - _document = Extension.ToThaiNumber(`คำสั่ง ${posNumCodeSitAbb ?? "-"} ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))}`) + _document = Extension.ToThaiNumber( + `คำสั่ง ${posNumCodeSitAbb ?? "-"} ที่ ${commandNo}/${commandYear > 2500 ? commandYear : commandYear + 543} ลว. ${Extension.ToThaiFullDate2(commandExcecuteDate)}`, + ); _commandTypename = commandTypeName; } // ค้นหาหน่วยงานที่รักษาการ const _posmasterAct = await this.posMasterActRepository.findOne({ where: { posMasterChildId: posMasterId ?? "", - statusReport: "DONE" - } + statusReport: "DONE", + }, }); if (_posmasterAct) { const _posMater = await this.posMasterRepo.findOne({ where: { - id: _posmasterAct.posMasterId + id: _posmasterAct.posMasterId, }, - relations:{ + relations: { orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true, - } + }, }); - let child4 = _posMater?.orgChild4?.orgChild4Name ?? "" - let child3 = _posMater?.orgChild3?.orgChild3Name ?? "" - let child2 = _posMater?.orgChild2?.orgChild2Name ?? "" - let child1 = _posMater?.orgChild1?.orgChild1Name ?? "" - let root = _posMater?.orgRoot?.orgRootName ?? "" + let child4 = _posMater?.orgChild4?.orgChild4Name ?? ""; + let child3 = _posMater?.orgChild3?.orgChild3Name ?? ""; + let child2 = _posMater?.orgChild2?.orgChild2Name ?? ""; + let child1 = _posMater?.orgChild1?.orgChild1Name ?? ""; + let root = _posMater?.orgRoot?.orgRootName ?? ""; _org = `${child4} ${child3} ${child2} ${child1} ${root}`.trim(); } _actpositions.push({ - date: + date: item.dateStart && item.dateEnd ? Extension.ToThaiNumber( `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, @@ -1763,14 +1778,14 @@ export class ProfileController extends Controller { position: item.position ? Extension.ToThaiNumber(item.position) : "", commandName: _commandTypename, agency: _org, - note: _posmasterAct && _posmasterAct?.posMasterOrder - ? Extension.ToThaiNumber(_posmasterAct?.posMasterOrder.toString()) - : "-", + note: + _posmasterAct && _posmasterAct?.posMasterOrder + ? Extension.ToThaiNumber(_posmasterAct?.posMasterOrder.toString()) + : "-", document: _document, }); } - } - else { + } else { _actpositions.push({ date: "", position: "", @@ -1786,7 +1801,7 @@ export class ProfileController extends Controller { where: { profileId: id, /*status: "PENDING",*/ isDeleted: false }, order: { createdAt: "ASC" }, }); - let _assistances = [] + let _assistances = []; if (assistance_raw.length > 0) { for (const item of assistance_raw) { let _commandTypename: string = item.commandName ?? "ให้ช่วยราชการ"; @@ -1794,19 +1809,16 @@ export class ProfileController extends Controller { let _org: string = item.agency ? Extension.ToThaiNumber(item.agency) : "-"; if (item.commandId) { - const { - posNumCodeSitAbb, - commandTypeName, - commandNo, - commandYear, - commandExcecuteDate, - } = await getPosNumCodeSit(item.commandId); + const { posNumCodeSitAbb, commandTypeName, commandNo, commandYear, commandExcecuteDate } = + await getPosNumCodeSit(item.commandId); - _document = Extension.ToThaiNumber(`คำสั่ง ${posNumCodeSitAbb ?? "-"} ที่ ${commandNo}/${commandYear>2500?commandYear:commandYear+543} ลว. ${(Extension.ToThaiFullDate2(commandExcecuteDate))}`) + _document = Extension.ToThaiNumber( + `คำสั่ง ${posNumCodeSitAbb ?? "-"} ที่ ${commandNo}/${commandYear > 2500 ? commandYear : commandYear + 543} ลว. ${Extension.ToThaiFullDate2(commandExcecuteDate)}`, + ); _commandTypename = commandTypeName; } _assistances.push({ - date: + date: item.dateStart && item.dateEnd ? Extension.ToThaiNumber( `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, @@ -1847,10 +1859,12 @@ export class ProfileController extends Controller { type: "-", detail: Extension.ToThaiNumber(item.detail), agency: "-", - refCommandNo: item.refCommandNo + refCommandNo: item.refCommandNo ? item.refCommandDate - ? Extension.ToThaiNumber(`${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`) - : Extension.ToThaiNumber(item.refCommandNo) + ? Extension.ToThaiNumber( + `${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`, + ) + : Extension.ToThaiNumber(item.refCommandNo) : "-", })) : [ @@ -1870,9 +1884,14 @@ export class ProfileController extends Controller { assessments_raw.length > 0 ? assessments_raw.map((item) => ({ year: item.year ? Extension.ToThaiNumber((parseInt(item.year) + 543).toString()) : "", - period: item.period && item.period == "APR" - ? Extension.ToThaiNumber(`1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`) - : Extension.ToThaiNumber(`1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`), + period: + item.period && item.period == "APR" + ? Extension.ToThaiNumber( + `1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`, + ) + : Extension.ToThaiNumber( + `1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`, + ), point1: item.point1 ? Extension.ToThaiNumber(item.point1.toString()) : "", point1Total: item.point1Total ? Extension.ToThaiNumber(item.point1Total.toString()) @@ -1881,8 +1900,8 @@ export class ProfileController extends Controller { point2Total: item.point2Total ? Extension.ToThaiNumber(item.point2Total.toString()) : "", - pointSum: item.pointSum - ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) + pointSum: item.pointSum + ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) : "", pointSumTh: item.pointSum ? Extension.textPoint(item.pointSum) : "", level: @@ -1935,33 +1954,41 @@ export class ProfileController extends Controller { const otherIncome = otherIncome_raw.length > 0 ? await Promise.all( - otherIncome_raw.map(async(item) => { - const codeSitAbb = item.posNumCodeSitAbb ?? "-" - const commandNo = item.commandNo && item.commandYear - ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) - : "-" - const dateAffect = item.commandDateAffect - ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` - : "-" - return { - commandName: item.commandName ?? "", - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", - position: item.positionName, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`) - } - })) + otherIncome_raw.map(async (item) => { + const codeSitAbb = item.posNumCodeSitAbb ?? "-"; + const commandNo = + item.commandNo && item.commandYear + ? item.commandNo + + "/" + + (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-"; + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-"; + return { + commandName: item.commandName ?? "", + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : "", + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", + position: item.positionName, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount + ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, + ), + }; + }), + ) : [ { commandName: "", @@ -1971,7 +1998,7 @@ export class ProfileController extends Controller { position: "", posLevel: "", amount: "", - refDoc: "" + refDoc: "", }, ]; @@ -2018,7 +2045,7 @@ export class ProfileController extends Controller { ) : ""; - let portfolios:any[] = []; + let portfolios: any[] = []; await new CallAPI() .GetData(req, `/development/portfolio/kk1/${profiles?.keycloak}`) .then((x) => { @@ -2031,7 +2058,7 @@ export class ProfileController extends Controller { portfolios = portfolios.map((x: any) => ({ name: x.name ? Extension.ToThaiNumber(x.name) : "-", year: x.year ? Extension.ToThaiNumber(x.year.toString()) : "-", - position: `${x.position ?? "-"}` + position: `${x.position ?? "-"}`, })); } const org = @@ -2039,19 +2066,21 @@ export class ProfileController extends Controller { (_child3 == null ? "" : _child3 + " ") + (_child2 == null ? "" : _child2 + " ") + (_child1 == null ? "" : _child1 + " ") + - (_root == null ? "" : _root).trim() - const _position = profiles?.position != null ? - profiles?.posLevel != null - ? `${profiles.position}${profiles.posLevel.posLevelName}` - : profiles.position - : "" + (_root == null ? "" : _root).trim(); + const _position = + profiles?.position != null + ? profiles?.posLevel != null + ? `${profiles.position}${profiles.posLevel.posLevelName}` + : profiles.position + : ""; const ocAssistance = await this.profileAssistanceRepository.findOne({ select: ["agency", "profileId", "commandName", "status", "isDeleted"], - where: { + where: { profileId: id, commandName: "ให้ช่วยราชการ", status: "PENDING", - isDeleted: false, }, + isDeleted: false, + }, order: { createdAt: "ASC" }, }); const data = { @@ -2080,7 +2109,8 @@ export class ProfileController extends Controller { ocFullPath: org, ocAssistance: ocAssistance?.agency ?? org, root: _root == null ? "" : _root, - agency: (_child4 == null ? "" : _child4 + " ") + + agency: + (_child4 == null ? "" : _child4 + " ") + (_child3 == null ? "" : _child3 + " ") + (_child2 == null ? "" : _child2 + " ") + (_child1 == null ? "" : _child1).trim(), @@ -2212,7 +2242,7 @@ export class ProfileController extends Controller { profileAbility, otherIncome, portfolios, - retires + retires, }; return new HttpSuccess({ @@ -5391,7 +5421,7 @@ export class ProfileController extends Controller { await this.profileRepo.save(record); // setLogDataDiff(request, { before, after: record }); - if (record != null && record.keycloak != null) { + if (record != null && record.keycloak != null && record.isDelete === false) { const result = await updateName( record.keycloak, record.firstName, @@ -5710,7 +5740,7 @@ export class ProfileController extends Controller { await this.profileRepo.save(record, { data: request }); setLogDataDiff(request, { before, after: record }); - if (record != null && record.keycloak != null) { + if (record != null && record.keycloak != null && record.isDelete === false) { const result = await updateName( record.keycloak, record.firstName, @@ -11260,13 +11290,14 @@ export class ProfileController extends Controller { // profile.position = _null; // profile.posLevelId = _null; // profile.posTypeId = _null; - if (profile.keycloak != null) { + 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; } } await this.profileRepo.save(profile, { data: request }); @@ -11372,7 +11403,10 @@ export class ProfileController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.isActive = :isActive", { isActive: false }) + .where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", { + isActive: false, + isDelete: true, + }) .andWhere( new Brackets((qb) => { qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` }); diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index ffdb2085..2a7fba38 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -871,7 +871,7 @@ export class ProfileEmployeeController extends Controller { }); // กรองเอา object ที่ไม่มี year ออก - const filteredLeaves = leaves.filter(item => item.year && item.year !== ""); + const filteredLeaves = leaves.filter((item) => item.year && item.year !== ""); const data = { fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, @@ -1135,7 +1135,9 @@ export class ProfileEmployeeController extends Controller { certificateType: item.certificateType ?? null, issuer: item.issuer ?? null, certificateNo: item.certificateNo ? Extension.ToThaiNumber(item.certificateNo) : null, - detail: Extension.ToThaiNumber(`${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim()), + detail: Extension.ToThaiNumber( + `${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim(), + ), issueToExpireDate: item.issueDate ? item.expireDate ? Extension.ToThaiNumber( @@ -1167,7 +1169,9 @@ export class ProfileEmployeeController extends Controller { degree: item.name ? Extension.ToThaiNumber(item.name) : "", place: item.place ? Extension.ToThaiNumber(item.place) : "", duration: item.duration ? Extension.ToThaiNumber(item.duration) : "", - date: Extension.ToThaiNumber(`${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`) + date: Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`, + ), })) : [ { @@ -1175,7 +1179,7 @@ export class ProfileEmployeeController extends Controller { degree: "", place: "", duration: "", - date: "" + date: "", }, ]; @@ -1218,14 +1222,14 @@ export class ProfileEmployeeController extends Controller { ? `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)) : ""}` : `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.startDate))) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.endDate))) : ""}`, degree: `${item.degree ?? ""} ${item.field ?? ""}`.trim(), - level: item.educationLevel + level: item.educationLevel, })) : [ { institute: "", date: "", degree: "", - level: "" + level: "", }, ]; const salary_raw = await this.salaryRepo.find({ @@ -1441,9 +1445,7 @@ export class ProfileEmployeeController extends Controller { // Merge มาสาย/ขาดราชการเข้า leaves array absentLate_raw.forEach((item) => { - const year = item.year - ? Extension.ToThaiNumber((item.year + 543).toString()) - : ""; + const year = item.year ? Extension.ToThaiNumber((item.year + 543).toString()) : ""; let yearData = leaves.find((data) => data.year === year); @@ -1477,7 +1479,7 @@ export class ProfileEmployeeController extends Controller { // }); if (leaves.length === 0) { - leaves.push({year:""}); + leaves.push({ year: "" }); } const leave2_raw = await this.profileLeaveRepository @@ -1509,12 +1511,12 @@ export class ProfileEmployeeController extends Controller { const displayType = leaveTypeCode === "LV-008" && item.leaveSubTypeName ? item.leaveSubTypeName - : (item.name || "-"); + : item.name || "-"; // ข้อที่ 2: แสดง reason ก่อนเสมอ ถ้ามี coupleDayLevelCountry ค่อยแสดงประเทศ const displayReason = item.coupleDayLevelCountry ? `${item.reason || ""} ลาไปประเทศ ${item.coupleDayLevelCountry}`.trim() - : (item.reason || "-"); + : item.reason || "-"; return { date: @@ -1606,79 +1608,93 @@ export class ProfileEmployeeController extends Controller { ]; const position_raw = await this.salaryRepo.find({ - where: [{ - profileEmployeeId: id, - commandCode: In(["0","1","2","3","4","8","9","10","11","12","13","14","15","16","20"]), - // isEntry: false, - }, - { profileEmployeeId: id, commandCode: IsNull() }], + where: [ + { + profileEmployeeId: id, + commandCode: In([ + "0", + "1", + "2", + "3", + "4", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "20", + ]), + // isEntry: false, + }, + { profileEmployeeId: id, commandCode: IsNull() }, + ], order: { order: "ASC" }, }); - let _commandName:any = ""; - let _commandDateAffect:any = "" + let _commandName: any = ""; + let _commandDateAffect: any = ""; const positionList = position_raw.length > 0 - ? await Promise.all(position_raw.map(async(item, idx, arr) => { - const isLast = idx === arr.length - 1; - if (isLast) { - _commandDateAffect = item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : ""; - } - const _code = item.commandCode ? Number(item.commandCode) : null; - if (_code != null) { - _commandName = await this.commandCodeRepository.findOne({ - select: { name: true, code: true }, - where: { code: _code } - }); - } - const codeSitAbb = item.posNumCodeSitAbb ?? "-" - const commandNo = item.commandNo && item.commandYear - ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) - : "-" - const dateAffect = item.commandDateAffect - ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` - : "-" - return { - commandName: _commandName && _commandName.name - ? _commandName.name - : item.commandName, - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber( - Extension.ToThaiFullDate2(item.commandDateAffect) - ) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber( - Extension.ToThaiFullDate2(item.commandDateSign) - ) - : "", - posNo: - item.posNoAbb && item.posNo - ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + ? await Promise.all( + position_raw.map(async (item, idx, arr) => { + const isLast = idx === arr.length - 1; + if (isLast) { + _commandDateAffect = item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : ""; + } + const _code = item.commandCode ? Number(item.commandCode) : null; + if (_code != null) { + _commandName = await this.commandCodeRepository.findOne({ + select: { name: true, code: true }, + where: { code: _code }, + }); + } + const codeSitAbb = item.posNumCodeSitAbb ?? "-"; + const commandNo = + item.commandNo && item.commandYear + ? item.commandNo + + "/" + + (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-"; + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-"; + return { + commandName: + _commandName && _commandName.name ? _commandName.name : item.commandName, + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) : "", - position: item.positionName, - posType: item.positionType, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount - ? Extension.ToThaiNumber( - Number(item.amount).toLocaleString() - ) - : "", - positionSalaryAmount: item.positionSalaryAmount - ? Extension.ToThaiNumber( - Number(item.positionSalaryAmount).toLocaleString() - ) - : "", - refDoc: Extension.ToThaiNumber( - `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}` - ), - }; - })) + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + posNo: + item.posNoAbb && item.posNo + ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + : "", + position: item.positionName, + posType: item.positionType, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount + ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) + : "", + positionSalaryAmount: item.positionSalaryAmount + ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, + ), + }; + }), + ) : [ { commandName: "", @@ -1690,7 +1706,7 @@ export class ProfileEmployeeController extends Controller { posLevel: "", amount: "", positionSalaryAmount: "", - refDoc: "" + refDoc: "", }, ]; @@ -1760,48 +1776,52 @@ export class ProfileEmployeeController extends Controller { // document: "", // }, // ]; - const actposition = [{ - date: "", - position: "", - commandName: "", - agency: "", - document: "", - }]; + const actposition = [ + { + date: "", + position: "", + commandName: "", + agency: "", + document: "", + }, + ]; const duty_raw = await this.dutyRepository.find({ where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, }); const duty = - duty_raw.length > 0 - ? duty_raw.map((item) => ({ - date: - item.dateStart && item.dateEnd - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) - : item.dateStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) - : item.dateEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) - : "", - type: "-", - detail: Extension.ToThaiNumber(item.detail), - agency: "-", - refCommandNo: item.refCommandNo - ? item.refCommandDate - ? Extension.ToThaiNumber(`${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`) - : Extension.ToThaiNumber(item.refCommandNo) - : "-", - })) - : [ - { - date: "", - type: "", - detail: "", - agency: "", - refCommandNo: "", - }, - ]; + duty_raw.length > 0 + ? duty_raw.map((item) => ({ + date: + item.dateStart && item.dateEnd + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) + : item.dateStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) + : item.dateEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) + : "", + type: "-", + detail: Extension.ToThaiNumber(item.detail), + agency: "-", + refCommandNo: item.refCommandNo + ? item.refCommandDate + ? Extension.ToThaiNumber( + `${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`, + ) + : Extension.ToThaiNumber(item.refCommandNo) + : "-", + })) + : [ + { + date: "", + type: "", + detail: "", + agency: "", + refCommandNo: "", + }, + ]; const assessments_raw = await this.profileAssessmentsRepository.find({ where: { profileEmployeeId: id, isDeleted: false }, order: { createdAt: "ASC" }, @@ -1810,9 +1830,14 @@ export class ProfileEmployeeController extends Controller { assessments_raw.length > 0 ? assessments_raw.map((item) => ({ year: item.year ? Extension.ToThaiNumber((parseInt(item.year) + 543).toString()) : "", - period: item.period && item.period == "APR" - ? Extension.ToThaiNumber(`1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`) - : Extension.ToThaiNumber(`1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`), + period: + item.period && item.period == "APR" + ? Extension.ToThaiNumber( + `1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`, + ) + : Extension.ToThaiNumber( + `1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`, + ), point1: item.point1 ? Extension.ToThaiNumber(item.point1.toString()) : "", point1Total: item.point1Total ? Extension.ToThaiNumber(item.point1Total.toString()) @@ -1821,8 +1846,8 @@ export class ProfileEmployeeController extends Controller { point2Total: item.point2Total ? Extension.ToThaiNumber(item.point2Total.toString()) : "", - pointSum: item.pointSum - ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) + pointSum: item.pointSum + ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) : "", pointSumTh: item.pointSum ? Extension.textPoint(item.pointSum) : "", level: @@ -1874,33 +1899,41 @@ export class ProfileEmployeeController extends Controller { const otherIncome = otherIncome_raw.length > 0 ? await Promise.all( - otherIncome_raw.map(async(item) => { - const codeSitAbb = item.posNumCodeSitAbb ?? "-" - const commandNo = item.commandNo && item.commandYear - ? item.commandNo+"/"+(item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) - : "-" - const dateAffect = item.commandDateAffect - ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` - : "-" - return { - commandName: item.commandName ?? "", - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", - position: item.positionName, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) : "", - refDoc: Extension.ToThaiNumber(`คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`) - } - })) + otherIncome_raw.map(async (item) => { + const codeSitAbb = item.posNumCodeSitAbb ?? "-"; + const commandNo = + item.commandNo && item.commandYear + ? item.commandNo + + "/" + + (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-"; + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` + : "-"; + return { + commandName: item.commandName ?? "", + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : "", + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", + position: item.positionName, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount + ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, + ), + }; + }), + ) : [ { commandName: "", @@ -1910,7 +1943,7 @@ export class ProfileEmployeeController extends Controller { position: "", posLevel: "", amount: "", - refDoc: "" + refDoc: "", }, ]; @@ -1957,19 +1990,24 @@ export class ProfileEmployeeController extends Controller { (_child3 == null ? "" : _child3 + " ") + (_child2 == null ? "" : _child2 + " ") + (_child1 == null ? "" : _child1 + " ") + - (_root == null ? "" : _root).trim() - const _position = (profiles?.position != null ? - profiles.posType != null && profiles?.posLevel != null - ? Extension.ToThaiNumber(`${profiles.position} ${profiles.posType.posTypeShortName} ${profiles.posLevel.posLevelName}`) - : profiles.position - : "").trim() + (_root == null ? "" : _root).trim(); + const _position = ( + profiles?.position != null + ? profiles.posType != null && profiles?.posLevel != null + ? Extension.ToThaiNumber( + `${profiles.position} ${profiles.posType.posTypeShortName} ${profiles.posLevel.posLevelName}`, + ) + : profiles.position + : "" + ).trim(); const ocAssistance = await this.profileAssistanceRepository.findOne({ select: ["agency", "profileEmployeeId", "commandName", "status", "isDeleted"], where: { profileEmployeeId: id, commandName: "ให้ช่วยราชการ", status: "PENDING", - isDeleted: false, }, + isDeleted: false, + }, order: { createdAt: "ASC" }, }); @@ -2041,7 +2079,8 @@ export class ProfileEmployeeController extends Controller { ocFullPath: org, ocAssistance: ocAssistance?.agency ?? org, root: _root == null ? "" : _root, - agency: (_child4 == null ? "" : _child4 + " ") + + agency: + (_child4 == null ? "" : _child4 + " ") + (_child3 == null ? "" : _child3 + " ") + (_child2 == null ? "" : _child2 + " ") + (_child1 == null ? "" : _child1).trim(), @@ -2172,7 +2211,7 @@ export class ProfileEmployeeController extends Controller { assessments, profileAbility, otherIncome, - retires + retires, }; return new HttpSuccess({ @@ -5684,13 +5723,14 @@ export class ProfileEmployeeController extends Controller { // profile.position = _null; // profile.posLevelId = _null; // profile.posTypeId = _null; - if (profile.keycloak != null) { + 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; } } await this.profileRepo.save(profile); @@ -6162,7 +6202,10 @@ export class ProfileEmployeeController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.isActive = :isActive", { isActive: false }) + .where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", { + isActive: false, + isDelete: true, + }) .andWhere( new Brackets((qb) => { qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` }); diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index a864710d..f5182deb 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -3583,13 +3583,14 @@ export class ProfileEmployeeTempController extends Controller { // profile.position = _null; // profile.posLevelId = _null; // profile.posTypeId = _null; - if (profile.keycloak != null) { + 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; } } await this.profileRepo.save(profile); diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 52b97622..0b4629da 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -277,6 +277,7 @@ export class KeycloakController extends Controller { // Task #228 // const _null: any = null; // profileEmp.keycloak = _null; + profileEmp.isDelete = true; profileEmp.roleKeycloaks = []; await this.profileEmpRepo.save(profileEmp); } @@ -284,6 +285,7 @@ export class KeycloakController extends Controller { // Task #228 // const _null: any = null; // profile.keycloak = _null; + profile.isDelete = true; profile.roleKeycloaks = []; await this.profileRepo.save(profile); return new HttpSuccess(); @@ -569,7 +571,7 @@ export class KeycloakController extends Controller { .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") // .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") - .where("profile.isActive = :isActive", { isActive: true }) + .where("profile.isDelete = :isDelete", { isDelete: false }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere( @@ -613,7 +615,7 @@ export class KeycloakController extends Controller { .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") // .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") - .where("profileEmployee.isActive = :isActive", { isActive: true }) + .where("profileEmployee.isDelete = :isDelete", { isDelete: false }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere({ employeeClass: "PERM" }) From f7e8729e60bc0697e8626f7dfa507b7dda21df1c Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 2 Apr 2026 16:58:37 +0700 Subject: [PATCH 174/178] fix create user isDelete = false --- src/controllers/UserController.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 0b4629da..a3384f78 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -137,6 +137,7 @@ export class KeycloakController extends Controller { profile.keycloak = userId; } profile.email = body.email == null ? _null : body.email; + profile.isDelete = false; await this.profileRepo.save(profile); // Update Keycloak with profile prefix after profile is loaded @@ -202,6 +203,7 @@ export class KeycloakController extends Controller { profile.keycloak = userId; } profile.email = body.email == null ? _null : body.email; + profile.isDelete = false; await this.profileEmpRepo.save(profile); // Update Keycloak with profile prefix after profile is loaded await updateUserAttributes(userId, { @@ -760,6 +762,7 @@ export class KeycloakController extends Controller { profile.keycloak = userId; } profile.email = body.email == null ? _null : body.email; + profile.isDelete = false; await this.profileEmpRepo.save(profile); // Update Keycloak with profile prefix after profile is loaded await updateUserAttributes(userId, { From 58dd4cfd603be66a900b569b23412ce6d53097c4 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 2 Apr 2026 17:00:36 +0700 Subject: [PATCH 175/178] fix isDelete = false check by isActive = true --- src/controllers/CommandController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 4fe95e78..42e9bb2d 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -4293,6 +4293,7 @@ export class CommandController extends Controller { profile.amount = item.amount ?? _null; profile.amountSpecial = item.amountSpecial ?? _null; profile.isActive = true; + profile.isDelete = false; } await this.profileRepository.save(profile); @@ -6646,6 +6647,7 @@ export class CommandController extends Controller { profile.isLeave = item.bodyProfile.isLeave; profile.isRetirement = false; profile.isActive = true; + profile.isDelete = false; profile.dateLeave = _null; profile.dateRetire = _dateRetire; profile.dateRetireLaw = _dateRetireLaw; From 2fd99aaa9441542c2d7d5577b8ddeab5189b5916 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 2 Apr 2026 17:02:20 +0700 Subject: [PATCH 176/178] =?UTF-8?q?fix=20=E0=B8=9B=E0=B8=A3=E0=B8=B0?= =?UTF-8?q?=E0=B8=A7=E0=B8=B1=E0=B8=95=E0=B8=B4=E0=B8=84=E0=B8=99=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=87=20#2405?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 45 +++++++++++++++++++++++----- src/services/PositionService.ts | 23 +++++++++++--- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 63cc937b..bf00c42e 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -6928,12 +6928,40 @@ export class CommandController extends Controller { await this.posMasterRepository.save(posMaster); // STEP 5: กำหนด position ใหม่ - const positionNew = await this.positionRepository.findOne({ - where: { - id: item.bodyPosition.positionId, - posMasterId: posMaster.id, // ใช้ id ของ posMaster ตัวใหม่ - }, - }); + // เช็คว่า posMaster เปลี่ยนจากเก่าเป็นใหม่หรือไม่ + const originalPosMasterId = item.bodyPosition.posmasterId; + const isPosMasterChanged = originalPosMasterId !== posMaster.id; + + let positionNew = null; + + if (isPosMasterChanged) { + // posMaster เปลี่ยน ต้องหา position ใหม่จากคุณสมบัติของ position เก่า + // 1. หา position เก่าจาก id ที่ส่งมา + const positionOld = await this.positionRepository.findOne({ + where: { id: item.bodyPosition.positionId }, + }); + + if (positionOld) { + // 2. ใช้ posTypeId + posLevelId + positionName หา position ใหม่ใน posMaster ตัวใหม่ + positionNew = await this.positionRepository.findOne({ + where: { + posMasterId: posMaster.id, // ใช้ posMaster ตัวใหม่ + posTypeId: positionOld.posTypeId, + posLevelId: positionOld.posLevelId, + positionName: positionOld.positionName, + }, + }); + } + } else { + // posMaster ไม่เปลี่ยน - ใช้วิธีเดิม + positionNew = await this.positionRepository.findOne({ + where: { + id: item.bodyPosition.positionId, + posMasterId: posMaster.id, + }, + }); + } + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; @@ -6947,7 +6975,10 @@ export class CommandController extends Controller { } await this.positionRepository.save(positionNew, { data: req }); } - await CreatePosMasterHistoryOfficer(posMaster.id, req); + // await CreatePosMasterHistoryOfficer(posMaster.id, req); + await CreatePosMasterHistoryOfficer(posMaster.id, req, null, { + positionId: positionNew?.id + }); } // Insignia if (_oldInsigniaIds.length > 0) { diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 651d374c..44916aee 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -8,6 +8,7 @@ import { PosMaster } from "../entities/PosMaster"; import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; import { PosMasterEmployeeTempHistory } from "../entities/PosMasterEmployeeTempHistory"; import { PosMasterHistory } from "../entities/PosMasterHistory"; +import { Position } from "../entities/Position"; import { ProfileEducation } from "../entities/ProfileEducation"; import { RequestWithUser } from "../middlewares/user"; @@ -15,12 +16,14 @@ export async function CreatePosMasterHistoryOfficer( posMasterId: string, request: RequestWithUser | null, type?: string | null, + positionData?: { positionId?: string } | null, ): Promise { try { await AppDataSource.transaction(async (manager) => { const repoPosmaster = manager.getRepository(PosMaster); const repoHistory = manager.getRepository(PosMasterHistory); const repoOrgRevision = manager.getRepository(OrgRevision); + const repoPosition = manager.getRepository(Position); const pm = await repoPosmaster.findOne({ where: { id: posMasterId }, @@ -51,10 +54,22 @@ export async function CreatePosMasterHistoryOfficer( }); const _null: any = null; const h = new PosMasterHistory(); - const selectedPosition = - pm.positions.length > 0 - ? pm.positions.find((p) => p.positionIsSelected === true) ?? null - : null; + + // query position โดยตรงจาก positionRepository + let selectedPosition: Position | null = null; + if (positionData?.positionId) { + selectedPosition = await repoPosition.findOne({ + where: { id: positionData.positionId }, + relations: { posLevel: true, posType: true, posExecutive: true }, + }); + } else { + // ใช้ logic เดิม หาจาก pm.positions ที่ positionIsSelected = true + selectedPosition = + pm.positions.length > 0 + ? pm.positions.find((p) => p.positionIsSelected === true) ?? null + : null; + } + h.ancestorDNA = pm.ancestorDNA ? pm.ancestorDNA : _null; if (!type || type != "DELETE") { if (checkCurrentRevision) { From 97e5b8abc38d65e789182fd32660f0b5dbe4411a Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 2 Apr 2026 18:14:10 +0700 Subject: [PATCH 177/178] fix bug --- src/controllers/UserController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index a3384f78..afc686e6 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -572,8 +572,8 @@ export class KeycloakController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - // .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") - .where("profile.isDelete = :isDelete", { isDelete: false }) + .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") + .andWhere("profile.isDelete = :isDelete", { isDelete: false }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere( @@ -616,8 +616,8 @@ export class KeycloakController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - // .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") - .where("profileEmployee.isDelete = :isDelete", { isDelete: false }) + .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") + .andWhere("profileEmployee.isDelete = :isDelete", { isDelete: false }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere({ employeeClass: "PERM" }) From 15ac8d05141e50c00b239acdc815a78b2f41523c Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 3 Apr 2026 15:36:25 +0700 Subject: [PATCH 178/178] =?UTF-8?q?=E0=B8=AA=E0=B9=88=E0=B8=87=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=A2=E0=B8=8A=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B9=84?= =?UTF-8?q?=E0=B8=9B=E0=B8=AD=E0=B8=AD=E0=B8=81=E0=B8=84=E0=B8=B3=E0=B8=AA?= =?UTF-8?q?=E0=B8=B1=E0=B9=88=E0=B8=87=20C-PM-25,=20C-PM-26=20=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B9=89=E0=B8=9B=E0=B8=B1=E0=B9=8A=E0=B8=A1=20comman?= =?UTF-8?q?dCode=20#2377?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 4711ca2a..da0ed3fe 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -2693,13 +2693,26 @@ export class CommandController extends Controller { const path = commandTypePath(commandCode); if (path == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ"); - await new CallAPI() + if (!["C-PM-26", "C-PM-25"].includes(commandCode)) { + await new CallAPI() .PostData(request, path, { refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), status: "REPORT", }) .then(async (res) => {}) .catch(() => {}); + } + else { + await new CallAPI() + .PostData(request, path, { + refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), + status: "REPORT", + commandTypeId: requestBody.commandTypeId, + commandCode: commandCode, + }) + .then(async (res) => {}) + .catch(() => {}); + } let order = command.commandRecives == null || command.commandRecives.length <= 0 ? 0