diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts index f24eee35..81dbbf66 100644 --- a/src/controllers/KeycloakSyncController.ts +++ b/src/controllers/KeycloakSyncController.ts @@ -187,48 +187,6 @@ 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 aa6908e2..c8975e43 100644 --- a/src/controllers/ScriptProfileOrgController.ts +++ b/src/controllers/ScriptProfileOrgController.ts @@ -9,7 +9,6 @@ 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; @@ -27,7 +26,6 @@ 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; @@ -39,11 +37,6 @@ 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 @@ -68,7 +61,7 @@ export class ScriptProfileOrgController extends Controller { }); // Query with optimized select - only fetch required fields - const [posMasters, posMasterEmployee, posMasterEmployeeTemp] = await Promise.all([ + const [posMasters, posMasterEmployee] = await Promise.all([ this.posMasterRepo.find({ where: { lastUpdatedAt: MoreThanOrEqual(windowStart), @@ -127,46 +120,16 @@ 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, - employeeTempPosCount: posMasterEmployeeTemp.length, - totalRecords: posMasters.length + posMasterEmployee.length + posMasterEmployeeTemp.length, + totalRecords: posMasters.length + posMasterEmployee.length, }); // Build payloads with proper profile type tracking - const payloads = this.buildPayloads(posMasters, posMasterEmployee, posMasterEmployeeTemp); + const payloads = this.buildPayloads(posMasters, posMasterEmployee); if (payloads.length === 0) { console.log("cronjobUpdateOrg: No records to process"); @@ -283,13 +246,12 @@ export class ScriptProfileOrgController extends Controller { } /** - * Build payloads from PosMaster, EmployeePosMaster, and EmployeeTempPosMaster records + * Build payloads from PosMaster and EmployeePosMaster records * Includes proper profile type tracking for accurate Keycloak sync */ private buildPayloads( posMasters: PosMaster[], posMasterEmployee: EmployeePosMaster[], - posMasterEmployeeTemp: EmployeeTempPosMaster[], ): OrgUpdatePayload[] { const payloads: OrgUpdatePayload[] = []; @@ -323,21 +285,6 @@ 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 c5759274..5b61e286 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -88,103 +88,40 @@ export class KeycloakAttributeService { } // If not found in Profile, try ProfileEmployee (ลูกจ้าง) - // First, get the profileEmployee to check employeeClass - const profileEmployeeBasic = await this.profileEmployeeRepo + 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 (!profileEmployeeBasic) { - // Return null values if no profile found + 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: null, - orgRootDnaId: null, - orgChild1DnaId: null, - orgChild2DnaId: null, - orgChild3DnaId: null, - orgChild4DnaId: null, - empType: null, - prefix: null, + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, }; } - // 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", "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 ( - 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", "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 ( - 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, @@ -250,103 +187,40 @@ export class KeycloakAttributeService { }; } } else { - // First, get the profileEmployee to check employeeClass - const profileEmployeeBasic = await this.profileEmployeeRepo + 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 (!profileEmployeeBasic) { + 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: null, - orgRootDnaId: null, - orgChild1DnaId: null, - orgChild2DnaId: null, - orgChild3DnaId: null, - orgChild4DnaId: null, - empType: null, - prefix: null, + profileId: profileEmployeeResult.id, + orgRootDnaId, + orgChild1DnaId, + orgChild2DnaId, + orgChild3DnaId, + orgChild4DnaId, + empType: profileEmployeeResult.employeeClass, + prefix: profileEmployeeResult.prefix, }; } - - // 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", "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 ( - 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", "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 ( - 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 { @@ -439,95 +313,6 @@ 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