import { AppDataSource } from "../database/data-source"; import { Profile } from "./../entities/Profile"; import { ProfileEmployee } from "../entities/ProfileEmployee"; 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 { Brackets, Repository } from "typeorm"; import Extension from "../interfaces/extension"; import { RequestWithUser } from "../middlewares/user"; interface LeaveFilter { page: number; pageSize: number; searchField?: "firstName" | "lastName" | "fullName" | "citizenId" | "position" | "posNo"; searchKeyword?: string; posType?: string; posLevel?: string; isProbation?: boolean; node?: number; nodeId?: string; isAll?: boolean; retireType?: string; sortBy?: string; sort: "ASC" | "DESC"; _data: DataPermission; } interface DataPermission { root: string | null; child1: string | null; child2: string | null; child3: string | null; child4: string | null; privilege: string; } interface OrganizationCondition { condition: string; params: Record; } interface NodeConfig { repository: Repository; nameField: string; condition: string; isAllTrue: string; paramKey: string; parentIdField: string; } interface NodeParams { [key: string]: string | null | undefined; } interface OrgParentName { orgRootName: string | null; orgChild1Name: string | null; orgChild2Name: string | null; orgChild3Name: string | null; orgChild4Name: string | null; } export class ProfileLeaveService { private profileEmployeeRepo: Repository; private profileRepo: Repository; private orgRootRepository: Repository; private child1Repository: Repository; private child2Repository: Repository; private child3Repository: Repository; private child4Repository: Repository; private readonly nodeConfigs: NodeConfig[]; constructor() { this.profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); this.profileRepo = AppDataSource.getRepository(Profile); this.orgRootRepository = AppDataSource.getRepository(OrgRoot); this.child1Repository = AppDataSource.getRepository(OrgChild1); this.child2Repository = AppDataSource.getRepository(OrgChild2); this.child3Repository = AppDataSource.getRepository(OrgChild3); this.child4Repository = AppDataSource.getRepository(OrgChild4); this.nodeConfigs = [ { repository: this.orgRootRepository, nameField: "orgRootName", condition: "profileSalary.orgRoot = :orgRoot", isAllTrue: "profileSalary.orgChild1 IS NULL", paramKey: "orgRoot", parentIdField: "", }, { repository: this.child1Repository, nameField: "orgChild1Name", condition: "profileSalary.orgChild1 = :orgChild1", isAllTrue: "profileSalary.orgChild2 IS NULL", paramKey: "orgChild1", parentIdField: "orgRootId", }, { repository: this.child2Repository, nameField: "orgChild2Name", condition: "profileSalary.orgChild2 = :orgChild2", isAllTrue: "profileSalary.orgChild3 IS NULL", paramKey: "orgChild2", parentIdField: "orgChild1Id", }, { repository: this.child3Repository, nameField: "orgChild3Name", condition: "profileSalary.orgChild3 = :orgChild3", isAllTrue: "profileSalary.orgChild4 IS NULL", paramKey: "orgChild3", parentIdField: "orgChild2Id", }, { repository: this.child4Repository, nameField: "orgChild4Name", condition: "profileSalary.orgChild4 = :orgChild4", isAllTrue: "", paramKey: "orgChild4", parentIdField: "orgChild3Id", }, ]; } /** สร้าง query สำหรับการค้นหาตามฟิลด์ต่างๆ */ buildSearchQuery(searchField?: string, type: string = "profile"): string { switch (searchField) { case "citizenId": return `${type}.citizenId LIKE :keyword`; case "position": return `${type}.position LIKE :keyword`; case "posNo": return ` (CONCAT(profileSalary.posNoAbb, profileSalary.posNo) LIKE :keyword) OR (CONCAT(profileSalary.posNoAbb, " ", profileSalary.posNo) LIKE :keyword) OR (profileSalary.posNo LIKE :keyword) `; default: return `CONCAT(${type}.prefix, ${type}.firstName, ' ', ${type}.lastName) LIKE :keyword`; } } async findOrgNodeParentAll(node: number, nodeId: string): Promise { const orgNames: OrgParentName = { orgRootName: null, orgChild1Name: null, orgChild2Name: null, orgChild3Name: null, orgChild4Name: null, }; if (!nodeId || node < 0 || node >= this.nodeConfigs.length) { return orgNames; } let currentNode = node; let currentNodeId = nodeId; while (currentNode >= 0) { const config = this.nodeConfigs[currentNode]; // Build select fields dynamically, excluding empty parentIdField const selectFields = [config.nameField, "id"]; if (config.parentIdField && config.parentIdField.trim() !== "") { selectFields.push(config.parentIdField); } const orgData = await config.repository.findOne({ where: { id: currentNodeId }, select: selectFields, }); if (!orgData) { break; } const orgName = orgData[config.nameField] || null; if (orgName) { orgNames[config.nameField as keyof OrgParentName] = orgName; } // Check if parentIdField exists and is not empty before accessing it if (config.parentIdField && config.parentIdField.trim() !== "") { currentNodeId = orgData[config.parentIdField]; currentNode -= 1; } else { // If no parent field (root level), break the loop break; } } return orgNames; } /** สร้างเงื่อนไขการค้นหาตาม node และ nodeId และเช็คกับ permission */ async buildNodeCondition( node: number, nodeId: string, isAll?: boolean, ): Promise { // Early return สำหรับ edge cases if (!nodeId || node < 0 || node >= this.nodeConfigs.length) { return { condition: "1=1", params: {} }; } let nodeCondition = ""; let params: NodeParams = {}; const orgLists = await this.findOrgNodeParentAll(node, nodeId); console.log("Org Hierarchy for Node Condition:", orgLists); await Promise.all( this.nodeConfigs.map(async (config, index) => { if (index <= node) { const orgName = orgLists[config.nameField as keyof OrgParentName] || null; if (orgName) { nodeCondition += index > 0 ? ` AND ${config.condition}` : config.condition; nodeCondition += isAll === false && config.isAllTrue ? ` AND ${config.isAllTrue}` : ""; params[config.paramKey] = orgName; } } }), ); return { condition: nodeCondition, params, }; } async getOrgNameFromId(orgIds: { root: string | null; child1: string | null; child2: string | null; child3: string | null; child4: string | null; }): Promise { const orgNames: OrgParentName = { orgRootName: null, orgChild1Name: null, orgChild2Name: null, orgChild3Name: null, orgChild4Name: null, }; if (orgIds.root) { const rootName = await this.orgRootRepository.findOne({ where: { id: orgIds.root }, select: ["orgRootName"], }); orgNames.orgRootName = rootName ? rootName.orgRootName : null; } if (orgIds.child1) { const child1 = await this.child1Repository.findOne({ where: { id: orgIds.child1 }, select: ["orgChild1Name"], }); orgNames.orgChild1Name = child1 ? child1.orgChild1Name : null; } if (orgIds.child2) { const child2 = await this.child2Repository.findOne({ where: { id: orgIds.child2 }, select: ["orgChild2Name"], }); orgNames.orgChild2Name = child2 ? child2.orgChild2Name : null; } if (orgIds.child3) { const child3 = await this.child3Repository.findOne({ where: { id: orgIds.child3 }, select: ["orgChild3Name"], }); orgNames.orgChild3Name = child3 ? child3.orgChild3Name : null; } if (orgIds.child4) { const child4 = await this.child4Repository.findOne({ where: { id: orgIds.child4 }, select: ["orgChild4Name"], }); orgNames.orgChild4Name = child4 ? child4.orgChild4Name : null; } return orgNames; } /** สร้างเงื่อนไขการค้นหาตาม node และ nodeId และเช็คกับ permission */ async buildPermissionCondition( _data: DataPermission, isAll?: boolean, ): Promise { // Early return สำหรับ OWNER privilege if (_data.privilege === "OWNER") { return { condition: "1=1", params: {} }; } // const nodeFields = ["root", "child1", "child2", "child3", "child4"] as const; let nodeCondition = ""; let params: NodeParams = {}; const orgLists = await this.getOrgNameFromId({ root: _data.root, child1: _data.child1, child2: _data.child2, child3: _data.child3, child4: _data.child4, }); // console.log("Org Hierarchy for Permission Condition:", orgLists); // check orgLists has at least one non-null value if ( !orgLists.orgRootName && !orgLists.orgChild1Name && !orgLists.orgChild2Name && !orgLists.orgChild3Name && !orgLists.orgChild4Name ) { return { condition: "1=0", params: {} }; // no access } await Promise.all( this.nodeConfigs.map(async (config, index) => { const orgName = orgLists[config.nameField as keyof OrgParentName] || null; if (orgName) { nodeCondition += index > 0 ? ` AND ${config.condition}` : config.condition; nodeCondition += isAll === false && config.isAllTrue ? ` AND ${config.isAllTrue}` : ""; params[config.paramKey] = orgName; } }), ); return { condition: nodeCondition, params, }; } /** แปลงข้อมูลลูกจ้างก่อน response */ transformEmployeeData(employee: ProfileEmployee): any { const dateEmployment = employee.profileEmployeeEmployment?.length === 0 || !employee.profileEmployeeEmployment ? null : employee.profileEmployeeEmployment.reduce((latest, current) => { return latest.date > current.date ? latest : current; }).date; // ตรวจสอบว่า profileSalary มีข้อมูลหรือไม่ const salary = employee.profileSalary && employee.profileSalary.length > 0 ? employee.profileSalary[0] : null; const posNo = salary?.posNoAbb && salary?.posNo ? `${salary.posNoAbb} ${salary.posNo}` : salary?.posNo || ""; // สร้าง organization hierarchy - ใช้ข้อมูลจาก temp fields ถ้า salary ไม่มี const org = salary ? [salary.orgChild4, salary.orgChild3, salary.orgChild2, salary.orgChild1, salary.orgRoot] .filter(Boolean) .join("\n") : [ employee.child4Temp, employee.child3Temp, employee.child2Temp, employee.child1Temp, employee.rootTemp, ] .filter(Boolean) .join("\n"); // สร้าง node information const getNodeInfo = (nodeTemp: string) => { switch (nodeTemp) { case "0": return { name: employee.rootTemp, shortName: employee.rootShortNameTemp, }; case "1": return { name: employee.child1Temp, shortName: employee.child1ShortNameTemp, }; case "2": return { name: employee.child2Temp, shortName: employee.child2ShortNameTemp, }; case "3": return { name: employee.child3Temp, shortName: employee.child3ShortNameTemp, }; case "4": return { name: employee.child4Temp, shortName: employee.child4ShortNameTemp, }; default: return { name: null, shortName: null }; } }; const nodeInfo = getNodeInfo(employee.nodeTemp || "0"); return { id: employee.id, avatar: employee.avatar, avatarName: employee.avatarName, prefix: employee.prefix, rank: employee.rank, firstName: employee.firstName, lastName: employee.lastName, citizenId: employee.citizenId, posLevel: employee.posLevel?.posLevelName || null, posType: employee.posType?.posTypeName || null, posTypeShortName: employee.posType?.posTypeShortName || null, posLevelId: employee.posLevel?.id || null, posTypeId: employee.posType?.id || null, positionId: employee.positionIdTemp, posmasterId: employee.posmasterIdTemp, position: employee.position, posNo, employeeClass: employee.employeeClass, govAge: Extension.CalculateGovAge(employee.dateAppoint, 0, 0), age: Extension.CalculateAgeStrV2(employee.birthDate, 0, 0, "GET"), dateEmployment, dateAppoint: employee.dateAppoint, dateStart: employee.dateStart, createdAt: employee.createdAt, dateRetireLaw: employee.dateRetireLaw, draftOrganizationOrganization: nodeInfo.name, draftPositionEmployee: employee.positionTemp, draftOrgEmployeeStatus: employee.statusTemp, node: employee.nodeTemp, nodeId: employee.nodeIdTemp, nodeName: nodeInfo.name, nodeShortName: nodeInfo.shortName, root: employee.rootTemp || null, rootId: employee.rootIdTemp || null, rootShortName: employee.rootShortNameTemp || null, child1: employee.child1Temp || null, child1Id: employee.child1IdTemp || null, child1ShortName: employee.child1ShortNameTemp || null, child2: employee.child2Temp || null, child2Id: employee.child2IdTemp || null, child2ShortName: employee.child2ShortNameTemp || null, child3: employee.child3Temp || null, child3Id: employee.child3IdTemp || null, child3ShortName: employee.child3ShortNameTemp || null, child4: employee.child4Temp || null, child4Id: employee.child4IdTemp || null, child4ShortName: employee.child4ShortNameTemp || null, org, }; } /** ค้นหาลูกจ้างที่พ้นจากราชการ */ async getLeaveEmployees( request: RequestWithUser, filter: LeaveFilter, ): Promise<{ data: any[]; total: number }> { const { page, pageSize, searchField, searchKeyword = "", posType, posLevel, isProbation, node, nodeId, isAll, retireType, sortBy = "profileEmployee.dateLeave", sort, _data, } = filter; const searchQuery = this.buildSearchQuery(searchField, "profileEmployee"); // สร้าง main query - เปลี่ยนจาก leftJoinAndSelect เป็น leftJoin สำหรับ profileSalary const queryBuilder = this.profileEmployeeRepo .createQueryBuilder("profileEmployee") .leftJoinAndSelect("profileEmployee.posLevel", "posLevel") .leftJoinAndSelect("profileEmployee.posType", "posType") .leftJoinAndSelect("profileEmployee.profileEmployeeEmployment", "profileEmployeeEmployment") .leftJoin( "profileEmployee.profileSalary", "profileSalary", "profileSalary.order = (SELECT MAX(ps.order) FROM profileSalary ps WHERE ps.profileEmployeeId = profileEmployee.id and ps.positionName != 'เกษียณอายุราชการ')", ) .addSelect([ "profileSalary.id", "profileSalary.order", "profileSalary.posNo", "profileSalary.posNoAbb", "profileSalary.orgRoot", "profileSalary.orgChild1", "profileSalary.orgChild2", "profileSalary.orgChild3", "profileSalary.orgChild4", ]) .where( new Brackets((qb) => { qb.where("profileEmployee.isLeave = :isLeave", { isLeave: true }).orWhere( "profileEmployee.isRetirement = :isRetirement", { isRetirement: true }, ); }), ) .andWhere("profileEmployee.employeeClass LIKE :type", { type: "PERM" }) .andWhere( new Brackets((qb) => { qb.orWhere(searchKeyword && searchKeyword != "" ? searchQuery : "1=1", { keyword: `%${searchKeyword}%`, }); }), ); // เพิ่มเงื่อนไขการค้นหา if (posType) { queryBuilder.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` }); } if (posLevel) { queryBuilder.andWhere( "CONCAT(posType.posTypeShortName, ' ', posLevel.posLevelName) LIKE :keyword2", { keyword2: `${posLevel}` }, ); } if (isProbation) { queryBuilder.andWhere(`profileEmployee.isProbation = ${isProbation}`); } if (retireType) { queryBuilder.andWhere("profileEmployee.leaveType = :retireType", { retireType }); } if (node !== null && node !== undefined && nodeId) { const [nodeCondition, permissionCondition] = await Promise.all([ this.buildNodeCondition(node, nodeId, isAll), this.buildPermissionCondition(_data, isAll), ]); // console.log("Permission Condition:", permissionCondition); // console.log("Node Condition:", nodeCondition); queryBuilder.andWhere(nodeCondition.condition, nodeCondition.params); if (_data.privilege !== "OWNER") { queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params); } } // เพิ่ม sorting และ pagination queryBuilder .orderBy(sortBy, sort) .skip((page - 1) * pageSize) .take(pageSize); const [records, total] = await queryBuilder.getManyAndCount(); // print query for debug // console.log("SQL Query:", queryBuilder.getSql()); const data = await Promise.all( records.map((record) => Promise.resolve(this.transformEmployeeData(record))), ); return { data, total }; } /** * แปลงข้อมูลลูกจ้างก่อน response */ transformOfficerData(employee: Profile) { // ตรวจสอบว่า profileSalary มีข้อมูลหรือไม่ const salary = employee.profileSalary && employee.profileSalary.length > 0 ? employee.profileSalary[0] : null; const posNo = salary?.posNoAbb && salary?.posNo ? `${salary.posNoAbb} ${salary.posNo}` : salary?.posNo || ""; const posExecutive = salary?.positionExecutive ? salary.positionExecutive : null; const root = salary?.orgRoot ? salary.orgRoot : null; // สร้าง organization hierarchy - ใช้ข้อมูลจาก temp fields ถ้า salary ไม่มี const org = salary ? [salary.orgChild4, salary.orgChild3, salary.orgChild2, salary.orgChild1, salary.orgRoot] .filter(Boolean) .join("\n") : ["", "", "", "", ""].filter(Boolean).join("\n"); const orgRootShortName = salary?.posNoAbb ? salary.posNoAbb : null; return { id: employee.id, avatar: employee.avatar, avatarName: employee.avatarName, dateAppoint: employee.dateAppoint, prefix: employee.prefix, rank: employee.rank, firstName: employee.firstName, lastName: employee.lastName, citizenId: employee.citizenId, posLevel: employee.posLevel?.posLevelName || null, posType: employee.posType?.posTypeName || null, posLevelId: employee.posLevel?.id || null, posTypeId: employee.posType?.id || null, position: employee.position, posExecutive, posNo, rootId: null, root, orgRootShortName, orgRevisionId: null, org, }; } /** * ค้นหาข้าราชการที่พ้นจากราชการ */ async getLeaveOfficer( request: RequestWithUser, filter: LeaveFilter, ): Promise<{ data: any[]; total: number }> { const { page, pageSize, searchField, searchKeyword = "", posType, posLevel, isProbation, node, nodeId, isAll, retireType, sortBy = "profile.dateLeave", sort, _data, } = filter; const searchQuery = this.buildSearchQuery(searchField); // สร้าง main query - เปลี่ยนจาก leftJoinAndSelect เป็น leftJoin สำหรับ profileSalary const queryBuilder = this.profileRepo .createQueryBuilder("profile") .leftJoinAndSelect("profile.posLevel", "posLevel") .leftJoinAndSelect("profile.posType", "posType") .leftJoin( "profile.profileSalary", "profileSalary", "profileSalary.order = (SELECT MAX(ps.order) FROM profileSalary ps WHERE ps.profileId = profile.id and ps.positionName != 'เกษียณอายุราชการ')", ) .addSelect([ "profileSalary.id", "profileSalary.order", "profileSalary.posNo", "profileSalary.posNoAbb", "profileSalary.orgRoot", "profileSalary.orgChild1", "profileSalary.orgChild2", "profileSalary.orgChild3", "profileSalary.orgChild4", "profileSalary.positionExecutive", ]) .where( new Brackets((qb) => { qb.where("profile.isLeave = :isLeave", { isLeave: true }).orWhere( "profile.isRetirement = :isRetirement", { isRetirement: true }, ); }), ) .andWhere( new Brackets((qb) => { qb.orWhere(searchKeyword && searchKeyword != "" ? searchQuery : "1=1", { keyword: `%${searchKeyword}%`, }); }), ); if (posType) { queryBuilder.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` }); } if (posLevel) { queryBuilder.andWhere("posLevel.posLevelName LIKE :keyword2", { keyword2: `${posLevel}` }); } if (isProbation) { queryBuilder.andWhere(`profile.isProbation = ${isProbation}`); } if (retireType) { queryBuilder.andWhere("profile.leaveType = :retireType", { retireType }); } // เพิ่ม permission และ node conditions if (node !== null && node !== undefined && nodeId) { // สร้าง query conditions แบบ parallel const [nodeCondition, permissionCondition] = await Promise.all([ this.buildNodeCondition(node, nodeId, isAll), this.buildPermissionCondition(_data, isAll), ]); console.log("Permission Condition:", permissionCondition); console.log("Node Condition:", nodeCondition); queryBuilder.andWhere(nodeCondition.condition, nodeCondition.params); if (_data.privilege !== "OWNER") { queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params); } } // เพิ่ม sorting และ pagination queryBuilder .orderBy(sortBy, sort) .skip((page - 1) * pageSize) .take(pageSize); const [records, total] = await queryBuilder.getManyAndCount(); // print query for debug // console.log("SQL Query:", queryBuilder.getSql()); const data = await Promise.all( records.map((record) => Promise.resolve(this.transformOfficerData(record))), ); return { data, total }; } }