import { Body, Controller, Get, Path, Post, Put, Request, Route, Security, Tags } from "tsoa"; import { AppDataSource } from "../database/data-source"; import { RequestWithUser } from "../middlewares/user"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import HttpSuccess from "../interfaces/http-success"; import { Workflow } from "../entities/Workflow"; import { State } from "../entities/State"; import { StateOperator } from "../entities/StateOperator"; import { StateOperatorUser } from "../entities/StateOperatorUser"; import CallAPI from "../interfaces/call-api"; import { Profile } from "../entities/Profile"; import { StateUserComment } from "../entities/StateUserComment"; import { MetaWorkflow } from "../entities/MetaWorkflow"; import { MetaState } from "../entities/MetaState"; import { MetaStateOperator } from "../entities/MetaStateOperator"; import { PosMaster } from "../entities/PosMaster"; import { Brackets, IsNull, Not, In } from "typeorm"; import { Assign } from "../entities/Assign"; import { PosMasterAct } from "../entities/PosMasterAct"; import { viewDirectorActing } from "../entities/view/viewDirectorActing"; import { viewDirector } from "../entities/view/viewDirector"; import { ProfileEmployee } from "../entities/ProfileEmployee"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; @Route("api/v1/org/workflow") @Tags("Workflow") @Security("bearerAuth") export class WorkflowController extends Controller { private workflowRepo = AppDataSource.getRepository(Workflow); private stateRepo = AppDataSource.getRepository(State); private stateOperatorRepo = AppDataSource.getRepository(StateOperator); private stateOperatorUserRepo = AppDataSource.getRepository(StateOperatorUser); private stateUserCommentRepo = AppDataSource.getRepository(StateUserComment); private profileRepo = AppDataSource.getRepository(Profile); private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); private metaWorkflowRepo = AppDataSource.getRepository(MetaWorkflow); private metaStateRepo = AppDataSource.getRepository(MetaState); private metaStateOperatorRepo = AppDataSource.getRepository(MetaStateOperator); private posMasterRepo = AppDataSource.getRepository(PosMaster); private posMasterEmpRepo = AppDataSource.getRepository(EmployeePosMaster); private posMasterActRepo = AppDataSource.getRepository(PosMasterAct); private assignRepo = AppDataSource.getRepository(Assign); @Post("add-workflow") public async checkWorkflow( @Request() req: RequestWithUser, @Body() body: { refId: string; sysName: string; posLevelName: string; posTypeName: string; fullName?: string | null; }, ) { // ขั้นที่ 1: ทำการค้นหา profile และ metaWorkflow แบบ parallel const [userProfileOfficer, userProfileEmployee, metaWorkflow] = await Promise.all([ this.profileRepo.findOne({ where: { keycloak: req.user.sub }, select: ["id", "keycloak"], }), this.profileEmployeeRepo.findOne({ where: { keycloak: req.user.sub }, select: ["id", "keycloak"], }), this.metaWorkflowRepo.findOne({ where: { sysName: body.sysName, posLevelName: body.posLevelName, posTypeName: body.posTypeName, }, }), ]); // กำหนด profile type และ profile let profileType = "OFFICER"; let profile: any = userProfileOfficer; if (!profile) { profileType = "EMPLOYEE"; profile = userProfileEmployee; if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); } if (!metaWorkflow) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบกระบวนการนี้ได้"); const meta = { createdUserId: req.user.sub, createdFullName: req.user.name, lastUpdateUserId: req.user.sub, lastUpdateFullName: req.user.name, createdAt: new Date(), lastUpdatedAt: new Date(), }; // ขั้นที่ 2: สร้าง workflow และดึง metaState แบบ parallel const workflow = new Workflow(); Object.assign(workflow, { ...metaWorkflow, id: undefined, ...meta, ...body, profileType: profileType, system: body.sysName, }); const [savedWorkflow, metaStates] = await Promise.all([ this.workflowRepo.save(workflow), this.metaStateRepo.find({ where: { metaWorkflowId: metaWorkflow.id }, order: { order: "ASC" }, }), ]); // ขั้นที่ 3: สร้าง states ทั้งหมดในครั้งเดียว const statesToCreate = metaStates.map((item) => { const state = new State(); Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta }); return state; }); const savedStates = await this.stateRepo.save(statesToCreate); // ขั้นที่ 4: อัปเดต workflow.stateId กับ state แรก const firstState = savedStates.find((state) => state.order === 1); if (firstState) { savedWorkflow.stateId = firstState.id; await this.workflowRepo.save(savedWorkflow); } // ขั้นที่ 5: ดึง metaStateOperators ทั้งหมดและสร้าง stateOperators const metaStateIds = metaStates.map((item) => item.id); const allMetaStateOperators = await this.metaStateOperatorRepo.find({ where: { metaStateId: In(metaStateIds) }, }); // สร้าง stateOperators ทั้งหมดในครั้งเดียว const stateOperatorsToCreate: StateOperator[] = []; allMetaStateOperators.forEach((metaStateOp) => { const correspondingState = savedStates.find( (state) => metaStates.find((metaState) => metaState.id === metaStateOp.metaStateId)?.order === state.order, ); if (correspondingState) { const stateOperator = new StateOperator(); Object.assign(stateOperator, { ...metaStateOp, id: undefined, stateId: correspondingState.id, ...meta, }); stateOperatorsToCreate.push(stateOperator); } }); await this.stateOperatorRepo.save(stateOperatorsToCreate); // ขั้นที่ 6: สร้าง StateOperatorUsers แบบ bulk const stateOperatorUsersToCreate: StateOperatorUser[] = []; let orderNum = 1; // เพิ่ม Owner ก่อน if (profile) { const ownerStateOperatorUser = new StateOperatorUser(); Object.assign(ownerStateOperatorUser, { profileId: profileType === "OFFICER" ? profile.id : null, profileEmployeeId: profileType !== "OFFICER" ? profile.id : null, profileType: profileType, operator: "Owner", order: orderNum, workflowId: savedWorkflow.id, ...meta, }); stateOperatorUsersToCreate.push(ownerStateOperatorUser); } // ดึงข้อมูล profileOfficers และสร้าง StateOperatorUsers const profileOfficers = await this.posMasterRepo.find({ where: { posMasterAssigns: { assignId: body.sysName }, orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: Not(IsNull()), // เพิ่มเงื่อนไขนี้เพื่อกรองเฉพาะที่มี current_holder }, relations: ["orgChild1"], select: ["current_holderId", "orgChild1"], // เลือกเฉพาะ field ที่จำเป็น }); // สร้าง StateOperatorUsers สำหรับ officers profileOfficers.forEach((item) => { if (item.current_holderId) { orderNum += 1; const isPersonnelOfficer = item.orgChild1?.isOfficer === true; const officerStateOperatorUser = new StateOperatorUser(); Object.assign(officerStateOperatorUser, { profileId: item.current_holderId, operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer", profileType: "OFFICER", order: orderNum, workflowId: savedWorkflow.id, ...meta, }); stateOperatorUsersToCreate.push(officerStateOperatorUser); } }); // บันทึก StateOperatorUsers ทั้งหมดในครั้งเดียว await this.stateOperatorUserRepo.save(stateOperatorUsersToCreate); // ขั้นที่ 7: ส่ง notification (ใช้ข้อมูลที่มีอยู่แล้วแทนการ query ใหม่) const firstStateOperators = stateOperatorsToCreate.filter((so) => savedStates.find((state) => state.id === so.stateId && state.order === 1), ); const notificationReceivers = stateOperatorUsersToCreate .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) .map((user) => ({ receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, notiLink: "", })); // ส่ง notification แบบ fire-and-forget new CallAPI() .PostData(req, "/placement/noti/profiles", { subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, receiverUserIds: notificationReceivers, payload: "", isSendMail: true, isSendInbox: true, isSendNotification: true, }) .catch((error) => { console.error("Error calling API:", error); }); return new HttpSuccess(); } @Post("check-iscan") public async checkIsCan( @Request() req: RequestWithUser, @Body() body: { workflowId: string; stateId: string; profileId: string; action: string; }, ) { let stateOperatorUser = await this.stateOperatorUserRepo.findOne({ where: { profileId: body.profileId, workflowId: body.workflowId, }, }); if (!stateOperatorUser) { stateOperatorUser = await this.stateOperatorUserRepo.findOne({ where: { profileEmployeeId: body.profileId, workflowId: body.workflowId, }, }); if (!stateOperatorUser) throw new HttpError(HttpStatus.NOT_FOUND, "ผู้ใช้งานนี้ไม่มีหน้าที่ในกระบวนการนี้"); } const operator = await this.stateOperatorRepo.findOne({ where: { operator: stateOperatorUser.operator, state: { id: body.stateId, workflow: { id: body.workflowId } }, }, }); if (!operator) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่สามารถดำเนินการกระบวนการนี้ได้"); let isCan = false; switch (body.action.trim().toLocaleUpperCase()) { case "VIEW": isCan = operator.canView; case "UPDATE": isCan = operator.canUpdate; case "DELETE": isCan = operator.canDelete; case "CANCEL": isCan = operator.canCancel; case "OPERATE": isCan = operator.canOperate; case "CHANGESTATE": isCan = operator.canChangeState; case "COMMENT": isCan = operator.canComment; case "SIGN": isCan = operator.canSign; default: isCan = false; } if (isCan == false) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่สามารถดำเนินการได้"); } else { return new HttpSuccess(); } } @Post("check-state-now") public async checkStateNow( @Request() req: RequestWithUser, @Body() body: { refId: string; system: string; }, ) { const workflow = await this.workflowRepo.findOne({ where: { refId: body.refId, sysName: body.system, }, relations: ["stateOperatorUsers"], }); if (!workflow) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่สามารถดำเนินการกระบวนการนี้ได้"); const state = await this.stateRepo.findOne({ where: { id: workflow.stateId, }, relations: ["stateOperators"], }); if (!state) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลขั้นตอนการอนุมัติ"); return new HttpSuccess({ stateId: state.id, stateNo: state.order, stateName: state.name, }); } @Post("check-user-now") public async checkUserNow( @Request() req: RequestWithUser, @Body() body: { refId: string; system: string; }, ) { const workflow = await this.workflowRepo.findOne({ where: { refId: body.refId, sysName: body.system, }, }); if (!workflow) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่สามารถดำเนินการกระบวนการนี้ได้"); let stateOperatorUser = await this.stateOperatorUserRepo.findOne({ where: { workflow: { refId: body.refId, sysName: body.system, }, profile: { keycloak: req.user.sub, }, }, relations: ["workflow"], }); if (!stateOperatorUser) { stateOperatorUser = await this.stateOperatorUserRepo.findOne({ where: { workflow: { refId: body.refId, sysName: body.system, }, profileEmployee: { keycloak: req.user.sub, }, }, relations: ["workflow"], }); } const operator = await this.stateOperatorRepo.findOne({ where: { operator: stateOperatorUser?.operator || "", stateId: workflow.stateId, }, relations: ["state"], }); if (!operator) { const state = await this.stateRepo.findOne({ where: { id: workflow.stateId, }, }); return new HttpSuccess({ stateId: state?.id || null, stateNo: state?.order || null, stateName: state?.name || null, operator: stateOperatorUser?.operator || null, can_view: false, can_update: false, can_operate: false, can_change_state: false, can_delete: false, can_cancel: false, can_sign: false, keycloakId: workflow.createdUserId, }); } return new HttpSuccess({ stateId: operator.state.id, stateNo: operator.state.order, stateName: operator.state.name, operator: operator.operator, can_view: operator.canView, can_update: operator.canUpdate, can_operate: operator.canOperate, can_change_state: operator.canChangeState, can_delete: operator.canDelete, can_cancel: operator.canCancel, can_sign: operator.canSign, keycloakId: workflow.createdUserId, }); } @Post("check-state-all") public async checkStateAll( @Request() req: RequestWithUser, @Body() body: { refId: string; system: string; }, ) { const state = await this.stateRepo.find({ where: { workflow: { refId: body.refId, sysName: body.system, }, }, order: { order: "ASC", stateUserComments: { order: "ASC" } }, relations: ["stateUserComments", "stateUserComments.profile"], }); const _state = state.map((x) => ({ stateId: x.id, stateNo: x.order, stateName: x.name, stateUserComments: x.stateUserComments.map((x) => ({ id: x.id, prefix: x.profile.prefix, firstName: x.profile.firstName, lastName: x.profile.lastName, isComment: x.profile.keycloak == req.user.sub ? true : false, profileId: x.profileId, isAcceptSetting: x.isAcceptSetting, isApproveSetting: x.isApproveSetting, isReasonSetting: x.isReasonSetting, isAccept: x.isAccept, isApprove: x.isApprove, reason: x.reason, })), })); return new HttpSuccess(_state); } @Post("state-next") public async stateNext( @Request() req: RequestWithUser, @Body() body: { refId: string; system: string; }, ) { const workflow = await this.workflowRepo.findOne({ where: { refId: body.refId, sysName: body.system, }, relations: ["stateOperatorUsers"], }); if (!workflow) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่สามารถดำเนินการกระบวนการนี้ได้"); let stateOperatorUserNow = await this.stateOperatorUserRepo.findOne({ where: { workflow: { refId: body.refId, sysName: body.system, }, profile: { keycloak: req.user.sub, }, }, }); if (!stateOperatorUserNow) { stateOperatorUserNow = await this.stateOperatorUserRepo.findOne({ where: { workflow: { refId: body.refId, sysName: body.system, }, profileEmployee: { keycloak: req.user.sub, }, }, }); } let stateOperatorUser = await this.stateOperatorUserRepo.find({ where: { workflow: { refId: body.refId, sysName: body.system, }, profile: { keycloak: Not(req.user.sub), }, operator: stateOperatorUserNow?.operator || "", }, }); if (!stateOperatorUser) { stateOperatorUser = await this.stateOperatorUserRepo.find({ where: { workflow: { refId: body.refId, sysName: body.system, }, profileEmployee: { keycloak: Not(req.user.sub), }, operator: stateOperatorUserNow?.operator || "", }, }); } await this.stateOperatorUserRepo.remove(stateOperatorUser); const state = await this.stateRepo.findOne({ where: { id: workflow.stateId, }, relations: ["stateOperators"], }); if (!state) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลขั้นตอนการอนุมัติ"); const _state = await this.stateRepo.findOne({ where: { order: state.order + 1, workflowId: state.workflowId, }, relations: ["stateOperators"], }); //noti let profileNow = workflow.stateOperatorUsers .filter((x) => state.stateOperators.map((s) => s.operator).includes(x.operator)) .map((x) => ({ receiverUserId: x.profileType == "OFFICER" ? x.profileId : x.profileEmployeeId, notiLink: "", })); await new CallAPI() .PostData(req, "/placement/noti/profiles", { subject: `รายการถูกส่ง`, body: `รายการถูกส่ง`, receiverUserIds: profileNow, payload: "", //แนบไฟล์ isSendMail: true, isSendInbox: true, isSendNotification: true, }) .catch((error) => { console.error("Error calling API:", error); }); if (_state != null) { let profileNext = workflow.stateOperatorUsers .filter((x) => _state.stateOperators.map((s) => s.operator).includes(x.operator)) .map((x) => ({ receiverUserId: x.profileType == "OFFICER" ? x.profileId : x.profileEmployeeId, notiLink: "", })); await new CallAPI() .PostData(req, "/placement/noti/profiles", { subject: `ได้รับรายการ ${workflow.name}`, body: `ได้รับรายการ ${workflow.name}`, receiverUserIds: profileNext, payload: "", //แนบไฟล์ isSendMail: true, isSendInbox: true, isSendNotification: true, }) .catch((error) => { console.error("Error calling API:", error); }); workflow.stateId = _state.id; await this.workflowRepo.save(workflow); } return new HttpSuccess({ stateId: _state?.id || null, stateNo: _state?.order || null, stateName: _state?.name || null, stateType: _state?.type || null, }); } @Post("state-back") public async stateBack( @Request() req: RequestWithUser, @Body() body: { stateId: string; }, ) { const state = await this.stateRepo.findOne({ where: { id: body.stateId, }, }); if (!state) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลขั้นตอนการอนุมัติ"); const _state = await this.stateRepo.findOne({ where: { order: state.order - 1, workflowId: state.workflowId, }, }); return new HttpSuccess({ stateId: _state?.id || null, stateNo: _state?.order || null, stateName: _state?.name || null, stateType: _state?.type || null, }); } @Post("add-step") public async addStep( @Request() req: RequestWithUser, @Body() body: { stateId: string; profileId: string; isAcceptSetting: boolean; isApproveSetting: boolean; isReasonSetting: boolean; }, ) { const profile = await this.profileRepo.findOne({ where: { id: body.profileId, }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบผู้ใช้งานนี้"); const state = await this.stateRepo.findOne({ where: { id: body.stateId, }, relations: ["stateUserComments", "workflow"], }); if (!state) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลขั้นตอนการอนุมัติ"); if (state.stateUserComments.filter((x) => x.profileId == body.profileId).length > 0) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่สามารถเลือกซ้ำได้"); const stateUserComment = new StateUserComment(); stateUserComment.order = state.stateUserComments.length + 1; stateUserComment.stateId = body.stateId; stateUserComment.profileId = body.profileId; stateUserComment.isAcceptSetting = body.isAcceptSetting; stateUserComment.isApproveSetting = body.isApproveSetting; stateUserComment.isReasonSetting = body.isReasonSetting; stateUserComment.createdUserId = req.user.sub; stateUserComment.createdFullName = req.user.name; stateUserComment.createdAt = new Date(); stateUserComment.lastUpdateUserId = req.user.sub; stateUserComment.lastUpdateFullName = req.user.name; stateUserComment.lastUpdatedAt = new Date(); await this.stateUserCommentRepo.save(stateUserComment); state.workflow.sysName; const assign = await this.assignRepo.findOne({ where: { id: state.workflow.sysName, }, }); if (!assign) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลขั้นตอนการอนุมัติ"); await new CallAPI() .PostData(req, "/placement/noti/profiles", { subject: `ได้รับรายการ ${state.workflow.name}`, body: `ได้รับรายการ ${state.workflow.name}`, receiverUserIds: [ { receiverUserId: body.profileId, notiLink: `${process.env.VITE_URL_MGT}/${assign.path}/${state.workflow.refId}`, }, ], payload: "", //แนบไฟล์ isSendMail: true, isSendInbox: true, isSendNotification: true, }) .catch((error) => { console.error("Error calling API:", error); }); return new HttpSuccess(); } @Post("comment") public async createcomment( @Request() req: RequestWithUser, @Body() body: { stateUserCommentId: string; isAccept?: boolean | null; isApprove?: boolean | null; reason?: string | null; }, ) { const stateUserComment = await this.stateUserCommentRepo.findOne({ where: { id: body.stateUserCommentId, }, relations: ["state", "state.workflow", "state.stateOperators"], }); if (!stateUserComment) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลขั้นตอนการอนุมัติ"); stateUserComment.state.stateOperators; const workflow = await this.workflowRepo.findOne({ where: { refId: stateUserComment.state.workflow.refId, sysName: stateUserComment.state.workflow.sysName, }, relations: ["stateOperatorUsers"], }); if (!workflow) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่สามารถดำเนินการกระบวนการนี้ได้"); let profileNow = workflow.stateOperatorUsers .filter((x) => stateUserComment.state.stateOperators.map((s) => s.operator).includes(x.operator), ) .map((x) => ({ receiverUserId: x.profileType == "OFFICER" ? x.profileId : x.profileEmployeeId, notiLink: "", })); await new CallAPI() .PostData(req, "/placement/noti/profiles", { subject: `ผู้บังคับบัญชาดำเนินการ`, body: `ผู้บังคับบัญชาดำเนินการ`, receiverUserIds: profileNow, payload: "", //แนบไฟล์ isSendMail: true, isSendInbox: true, isSendNotification: true, }) .catch((error) => { console.error("Error calling API:", error); }); let _null: any = null; stateUserComment.isAccept = body.isAccept == null ? _null : body.isAccept; stateUserComment.isApprove = body.isApprove == null ? _null : body.isApprove; stateUserComment.reason = body.reason == null ? _null : body.reason; stateUserComment.lastUpdateUserId = req.user.sub; stateUserComment.lastUpdateFullName = req.user.name; stateUserComment.lastUpdatedAt = new Date(); await this.stateUserCommentRepo.save(stateUserComment); return new HttpSuccess(); } @Post("comment-state") public async getCommentState( @Request() req: RequestWithUser, @Body() body: { stateId: string; }, ) { const state = await this.stateRepo.findOne({ where: { id: body.stateId, }, order: { stateUserComments: { order: "ASC" } }, relations: ["stateUserComments"], }); if (!state) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลขั้นตอนการอนุมัติ"); return new HttpSuccess(state.stateUserComments); } @Post("comment-state-user") public async getCommentStateUser( @Request() req: RequestWithUser, @Body() body: { stateId: string; }, ) { const stateUserComment = await this.stateUserCommentRepo.findOne({ where: { profile: { keycloak: req.user.sub, }, stateId: body.stateId, }, }); return new HttpSuccess({ id: stateUserComment?.id || null, isAccept: stateUserComment?.isAccept || null, isApprove: stateUserComment?.isApprove || null, reason: stateUserComment?.reason || null, isAcceptSetting: stateUserComment?.isAcceptSetting || null, isApproveSetting: stateUserComment?.isApproveSetting || null, isReasonSetting: stateUserComment?.isReasonSetting || null, order: stateUserComment?.order || null, stateId: stateUserComment?.stateId || null, profileId: stateUserComment?.profileId || null, }); } /** * * */ @Put("commander/{type}") async getProfilePlacement( @Request() request: RequestWithUser, @Path() type: string, @Body() body: { isAct: boolean; keyword: string; page: number; pageSize: number; keycloakId?: string | null; type?: string | null; sortBy?: string | null; descending?: boolean; }, ) { const userKeycloak = body.keycloakId ?? request.user.sub; // 1. Cache user lookup - ใช้ select เฉพาะ field ที่จำเป็น const userSelectFields = { id: true, orgRootId: true, orgChild1Id: true, orgChild2Id: true, orgChild3Id: true, orgChild4Id: true, orgRevisionId: true, current_holder: { id: true, posType: { id: true, posTypeName: true }, posLevel: { id: true, posLevelName: true }, }, }; let posMasterUser: any = null; if (body.type === "employee") { posMasterUser = await this.posMasterEmpRepo.findOne({ where: { orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, current_holder: { keycloak: userKeycloak }, }, select: userSelectFields, relations: ["current_holder", "current_holder.posType", "current_holder.posLevel"], }); } else { posMasterUser = await this.posMasterRepo.findOne({ where: { orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, current_holder: { keycloak: userKeycloak }, }, select: userSelectFields, relations: ["current_holder", "current_holder.posType", "current_holder.posLevel"], }); } if (!posMasterUser?.orgRootId) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบตำแหน่งผู้ใช้งาน"); } // 2. Pre-calculate conditions - ย้ายออกมาข้างนอก const posType = posMasterUser.current_holder?.posType?.posTypeName; const posLevel = posMasterUser.current_holder?.posLevel?.posLevelName; const isLowLevel = (posType === "ทั่วไป" && ["ชำนาญงาน", "ปฏิบัติงาน"].includes(posLevel)) || (posType === "วิชาการ" && ["ปฏิบัติการ", "ชำนาญการ"].includes(posLevel)); const isMidLevel = (posType === "ทั่วไป" && posLevel === "อาวุโส") || (posType === "วิชาการ" && posLevel === "ชำนาญการพิเศษ") || (posType === "อำนวยการ" && posLevel === "ต้น"); // 3. สร้าง conditions แบบ optimized const baseCondition = { isDirector: true, orgRevisionId: posMasterUser.orgRevisionId, }; const conditionOfficer = { ...baseCondition, isOfficer: true, orgChild1Id: IsNull(), }; let mainConditions: any[] = []; if (type.trim().toUpperCase() === "OPERATE" || body.type === "employee") { mainConditions = [ { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: IsNull() }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: IsNull(), }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: IsNull(), }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: posMasterUser.orgChild3Id, orgChild4Id: IsNull(), }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: posMasterUser.orgChild3Id, orgChild4Id: posMasterUser.orgChild4Id, }, ]; } else if (isLowLevel) { mainConditions = [ { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: IsNull(), orgChild2Id: IsNull(), orgChild3Id: IsNull(), orgChild4Id: IsNull(), }, ]; } else if (isMidLevel) { mainConditions = [ { ...baseCondition, isDeputy: true, orgChild1Id: IsNull(), orgChild2Id: IsNull(), orgChild3Id: IsNull(), orgChild4Id: IsNull(), }, ]; } else { mainConditions = [{ ...baseCondition, orgRootId: posMasterUser.orgRootId }]; } // 4. สร้าง optimized query builder const repository = body.isAct ? AppDataSource.getRepository(viewDirectorActing) : AppDataSource.getRepository(viewDirector); let queryBuilder = repository.createQueryBuilder("entity"); // 5. แยก WHERE conditions ให้เร็วขึ้น queryBuilder.where( new Brackets((qb) => { mainConditions.forEach((condition, index) => { if (index === 0) { qb.where(condition); } else { qb.orWhere(condition); } }); qb.orWhere(conditionOfficer); }), ); // 6. ปรับ search conditions ให้เร็วขึ้น if (body.keyword?.trim()) { const keyword = `%${body.keyword.trim()}%`; const searchFields = [ "CONCAT(entity.prefix, entity.firstName, ' ', entity.lastName)", "entity.citizenId", "entity.position", "entity.posLevel", "entity.posType", "entity.posNo", "entity.posExecutiveName", ]; if (body.isAct) { searchFields.push("entity.actFullName"); } queryBuilder.andWhere( `(${searchFields.map((field) => `${field} LIKE :keyword`).join(" OR ")})`, { keyword }, ); } if (body.sortBy) { queryBuilder = queryBuilder.orderBy( `entity.${body.sortBy}`, body.descending ? "DESC" : "ASC", ); } // 7. Execute พร้อมกัน - ใช้ Promise.all const [data, total] = await Promise.all([ queryBuilder .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getMany(), queryBuilder.getCount(), ]); // 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 ?? ""), })); return new HttpSuccess({ data: processedData, total }); } /** * * */ @Put("commander-posexe/{type}") async getProfilePlacementPosExe( @Request() request: RequestWithUser, @Path() type: string, @Body() body: { isAct: boolean; keyword: string; page: number; pageSize: number; keycloakId?: string | null; }, ) { const userKeycloak = body.keycloakId ?? request.user.sub; // 1. ใช้ select เฉพาะ field ที่จำเป็น - เหมือน getProfilePlacement const userSelectFields = { id: true, orgRootId: true, orgChild1Id: true, orgChild2Id: true, orgChild3Id: true, orgChild4Id: true, orgRevisionId: true, current_holder: { id: true, posType: { id: true, posTypeName: true }, posLevel: { id: true, posLevelName: true }, }, }; const posMasterUser = await this.posMasterRepo.findOne({ where: { orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, current_holder: { keycloak: userKeycloak }, }, select: userSelectFields, relations: ["current_holder", "current_holder.posType", "current_holder.posLevel"], }); if (!posMasterUser?.orgRootId) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบตำแหน่งผู้ใช้งาน"); } // 2. Pre-calculate conditions - ปรับให้เหมือน getProfilePlacement const posType = posMasterUser.current_holder?.posType?.posTypeName; const posLevel = posMasterUser.current_holder?.posLevel?.posLevelName; const isLowLevel = (posType === "ทั่วไป" && ["ชำนาญงาน", "ปฏิบัติงาน"].includes(posLevel)) || (posType === "วิชาการ" && ["ปฏิบัติการ", "ชำนาญการ"].includes(posLevel)); const isMidLevel = (posType === "ทั่วไป" && posLevel === "อาวุโส") || (posType === "วิชาการ" && posLevel === "ชำนาญการพิเศษ") || (posType === "อำนวยการ" && posLevel === "ต้น"); // 3. สร้าง conditions แบบ optimized const baseCondition = { isDirector: true, orgRevisionId: posMasterUser.orgRevisionId, }; const conditionOfficer = { ...baseCondition, isOfficer: true, orgChild1Id: IsNull(), }; let mainConditions: any[] = []; if (type.trim().toUpperCase() === "OPERATE") { mainConditions = [ { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: IsNull() }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: IsNull(), }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: IsNull(), }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: posMasterUser.orgChild3Id, orgChild4Id: IsNull(), }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: posMasterUser.orgChild1Id, orgChild2Id: posMasterUser.orgChild2Id, orgChild3Id: posMasterUser.orgChild3Id, orgChild4Id: posMasterUser.orgChild4Id, }, ]; } else if (isLowLevel) { mainConditions = [ { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: IsNull(), orgChild2Id: IsNull(), orgChild3Id: IsNull(), orgChild4Id: IsNull(), }, ]; } else if (isMidLevel) { mainConditions = [ { ...baseCondition, isDeputy: true, orgChild1Id: IsNull(), orgChild2Id: IsNull(), orgChild3Id: IsNull(), orgChild4Id: IsNull(), }, ]; } else { mainConditions = [{ ...baseCondition, orgRootId: posMasterUser.orgRootId }]; } // 4. สร้าง optimized query builder const repository = body.isAct ? AppDataSource.getRepository(viewDirectorActing) : AppDataSource.getRepository(viewDirector); const queryBuilder = repository.createQueryBuilder("entity"); // 5. แยก WHERE conditions ให้เร็วขึ้น queryBuilder.where( new Brackets((qb) => { mainConditions.forEach((condition, index) => { if (index === 0) { qb.where(condition); } else { qb.orWhere(condition); } }); qb.orWhere(conditionOfficer); }), ); // 6. ปรับ search conditions ให้เร็วขึ้น - แบบเดียวกับ getProfilePlacement if (body.keyword?.trim()) { const keyword = `%${body.keyword.trim()}%`; const searchFields = [ "CONCAT(entity.prefix, entity.firstName, ' ', entity.lastName)", "entity.citizenId", "entity.position", "entity.posLevel", "entity.posType", "entity.posNo", "entity.posExecutiveName", ]; if (body.isAct) { searchFields.push("entity.actFullName"); } queryBuilder.andWhere( `(${searchFields.map((field) => `${field} LIKE :keyword`).join(" OR ")})`, { keyword }, ); } // 7. Execute พร้อมกัน - ใช้ Promise.all const [data, total] = await Promise.all([ queryBuilder .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getMany(), queryBuilder.getCount(), ]); // 8. ปรับ response mapping 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 }); } /** * API เช็ค สกจ * * @summary เช็ค สกจ * */ @Get("keycloak/isofficer/{system}") async getIsOfficerByKeycloak(@Path() system: string, @Request() req: RequestWithUser) { const profile = await this.profileRepo.findOne({ where: { keycloak: req.user.sub, }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); const profileOfficer = await this.posMasterRepo.findOne({ where: { posMasterAssigns: { assignId: system }, orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: profile.id, }, relations: ["orgChild1", "orgRoot"], }); const profileDirector = await this.posMasterRepo.findOne({ where: { orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: profile.id, }, }); if (!profileOfficer) return new HttpSuccess({ isOfficer: false, isStaff: false, isDirector: profileDirector?.isDirector ?? false, isDeputy: false, }); let isOfficer = profileOfficer.orgChild1 == null ? false : profileOfficer.orgChild1.isOfficer; return new HttpSuccess({ isOfficer: isOfficer, isStaff: !isOfficer, isDirector: profileDirector?.isDirector ?? false, isDeputy: profileOfficer?.orgRoot?.isDeputy ?? false, }); } /** * API เช็ค สกจ * * @summary เช็ค สกจ * */ @Get("keycloak/isofficer-root/{system}") async getIsOfficerByKeycloakRoot(@Path() system: string, @Request() req: RequestWithUser) { const profile = await this.profileRepo.findOne({ where: { keycloak: req.user.sub, }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); const profileOfficer = await this.posMasterRepo.findOne({ where: { posMasterAssigns: { assignId: system }, orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: profile.id, }, relations: ["orgChild1"], }); const profileDirector = await this.posMasterRepo.findOne({ where: { orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: profile.id, }, }); if (!profileOfficer) return new HttpSuccess({ isOfficer: false, isStaff: false, isDirector: profileDirector?.isDirector ?? false, }); let isOfficer = profileOfficer.orgChild1 == null ? false : profileOfficer.orgChild1.isOfficer; return new HttpSuccess({ isOfficer: isOfficer, isStaff: !isOfficer, isDirector: profileDirector ? !profileDirector.orgChild1Id && profileDirector.orgRootId ? profileDirector.isDirector : false : false, }); } /** * API เช็ค สกจ * * @summary เช็ค สกจ * */ @Post("keycloak/isofficer") async checkPermissionWorkflow( @Request() req: RequestWithUser, @Body() body: { refId: string; sysName: string; }, ) { const profile = await this.profileRepo.findOne({ where: { keycloak: req.user.sub, }, }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); const profileOfficer = await this.workflowRepo.findOne({ where: { states: { stateUserComments: { profile: { keycloak: req.user.sub } } }, refId: body.refId, // sysName: body.sysName, }, }); if (!profileOfficer) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); return new HttpSuccess(); } /** * API หาหัวหน้า * * @summary หาหัวหน้า * */ @Post("find/director") async getProfileDirectorByProfileId( @Request() req: RequestWithUser, @Body() body: { refId: string[]; }, ) { const _posMaster = await this.posMasterRepo.find({ where: { orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: In(body.refId), }, select: ["orgRootId", "orgChild1Id", "orgChild2Id", "orgChild3Id", "orgChild4Id"], }); const _data: any = _posMaster.map((x) => ({ orgRootId: x.orgRootId, orgChild1Id: x.orgChild1Id, orgChild2Id: x.orgChild2Id, orgChild3Id: x.orgChild3Id, orgChild4Id: x.orgChild4Id, isDirector: true, current_holder: Not(IsNull()), })); const posMaster = await this.posMasterRepo.find({ where: _data, relations: ["current_holder"], }); const data = posMaster.map((x) => ({ id: x.current_holder.id, citizenId: x.current_holder.citizenId, prefix: x.current_holder.prefix, firstName: x.current_holder.firstName, lastName: x.current_holder.lastName, })); return new HttpSuccess(data); } /** * API หา กจ ตามระบบ * * @summary หา กจ ตามระบบ * */ @Post("find/director/{system}") async getProfileDirectorByProfileIdSystem( @Request() req: RequestWithUser, @Path() system: string, @Body() body: { refId: string[]; }, ) { const _posMaster = await this.posMasterRepo.find({ where: { orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: In(body.refId), }, select: ["orgRootId", "orgChild1Id", "orgChild2Id", "orgChild3Id", "orgChild4Id"], }); const _data: any = _posMaster.map((x) => ({ orgRootId: x.orgRootId, // orgChild1Id: x.orgChild1Id, // orgChild2Id: x.orgChild2Id, // orgChild3Id: x.orgChild3Id, // orgChild4Id: x.orgChild4Id, // isDirector: true, current_holder: Not(IsNull()), posMasterAssigns: { assignId: system.trim().toUpperCase() }, })); const posMaster = await this.posMasterRepo.find({ where: _data, relations: ["current_holder"], }); const data = posMaster.map((x) => ({ id: x.current_holder.id, citizenId: x.current_holder.citizenId, prefix: x.current_holder.prefix, firstName: x.current_holder.firstName, lastName: x.current_holder.lastName, })); return new HttpSuccess(data); } /** * API หา กจ ตามระบบด้วย keycloak * * @summary หา กจ ตามระบบด้วย keycloak * */ @Post("find/director-with-keycloak/{system}") async getProfileDirectorByKeycloakIdSystem( @Request() req: RequestWithUser, @Path() system: string, @Body() body: { refId: string[]; }, ) { const profileWithKc = await this.profileRepo.find({ where: { keycloak: In(body.refId), }, }); const profileIds = profileWithKc.map((p) => p.id); const _posMaster = await this.posMasterRepo.find({ where: { orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true, }, current_holderId: In(profileIds), }, select: ["orgRootId", "orgChild1Id", "orgChild2Id", "orgChild3Id", "orgChild4Id"], }); const _data: any = _posMaster.map((x) => ({ orgRootId: x.orgRootId, // orgChild1Id: x.orgChild1Id, // orgChild2Id: x.orgChild2Id, // orgChild3Id: x.orgChild3Id, // orgChild4Id: x.orgChild4Id, // isDirector: true, current_holder: Not(IsNull()), posMasterAssigns: { assignId: system.trim().toUpperCase() }, })); const posMaster = await this.posMasterRepo.find({ where: _data, relations: ["current_holder"], }); const data = posMaster.map((x) => ({ id: x.current_holder.id, keycloak: x.current_holder.keycloak, citizenId: x.current_holder.citizenId, prefix: x.current_holder.prefix, firstName: x.current_holder.firstName, lastName: x.current_holder.lastName, })); return new HttpSuccess(data); } }