From 49a8494a8d5ade7f9ef3cebaa14ee843073b82cd Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 27 Feb 2026 11:40:56 +0700 Subject: [PATCH] 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