2025-10-02 18:42:14 +07:00
import { AppDataSource } from "../database/data-source" ;
2025-10-03 13:05:24 +07:00
import { Profile } from "./../entities/Profile" ;
2025-10-02 18:42:14 +07:00
import { ProfileEmployee } from "../entities/ProfileEmployee" ;
2026-05-22 13:33:22 +07:00
import { ProfileSalary } from "./../entities/ProfileSalary" ;
2025-10-02 18:42:14 +07:00
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" ;
2026-05-22 13:33:22 +07:00
import { Brackets , In , Repository } from "typeorm" ;
2025-10-02 18:42:14 +07:00
import Extension from "../interfaces/extension" ;
import { RequestWithUser } from "../middlewares/user" ;
2025-10-04 13:54:17 +07:00
interface LeaveFilter {
2025-10-02 18:42:14 +07:00
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" ;
2025-10-04 13:54:17 +07:00
_data : DataPermission ;
2025-10-02 18:42:14 +07:00
}
2025-10-04 13:54:17 +07:00
interface DataPermission {
root : string | null ;
child1 : string | null ;
child2 : string | null ;
child3 : string | null ;
child4 : string | null ;
privilege : string ;
}
interface OrganizationCondition {
2025-10-02 18:42:14 +07:00
condition : string ;
params : Record < string , any > ;
}
2025-10-04 13:54:17 +07:00
interface NodeConfig {
repository : Repository < any > ;
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 ;
}
2025-10-02 18:42:14 +07:00
export class ProfileLeaveService {
2025-10-03 13:05:24 +07:00
private profileEmployeeRepo : Repository < ProfileEmployee > ;
private profileRepo : Repository < Profile > ;
2026-05-22 13:33:22 +07:00
private profileSalaryRepo : Repository < ProfileSalary > ;
2025-10-02 18:42:14 +07:00
private orgRootRepository : Repository < OrgRoot > ;
private child1Repository : Repository < OrgChild1 > ;
private child2Repository : Repository < OrgChild2 > ;
private child3Repository : Repository < OrgChild3 > ;
private child4Repository : Repository < OrgChild4 > ;
2025-10-04 13:54:17 +07:00
private readonly nodeConfigs : NodeConfig [ ] ;
2025-10-02 18:42:14 +07:00
constructor ( ) {
2025-10-03 13:05:24 +07:00
this . profileEmployeeRepo = AppDataSource . getRepository ( ProfileEmployee ) ;
this . profileRepo = AppDataSource . getRepository ( Profile ) ;
2026-05-22 13:33:22 +07:00
this . profileSalaryRepo = AppDataSource . getRepository ( ProfileSalary ) ;
2025-10-02 18:42:14 +07:00
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 ) ;
2025-10-04 13:54:17 +07:00
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" ,
} ,
] ;
2025-10-02 18:42:14 +07:00
}
2025-10-03 13:05:24 +07:00
/** สร้าง query สำหรับการค้นหาตามฟิลด์ต่างๆ */
buildSearchQuery ( searchField? : string , type : string = "profile" ) : string {
2025-10-02 18:42:14 +07:00
switch ( searchField ) {
case "citizenId" :
2025-10-03 13:05:24 +07:00
return ` ${ type } .citizenId LIKE :keyword ` ;
2025-10-02 18:42:14 +07:00
case "position" :
2025-10-03 13:05:24 +07:00
return ` ${ type } .position LIKE :keyword ` ;
2025-10-02 18:42:14 +07:00
case "posNo" :
return `
2025-10-03 13:05:24 +07:00
( CONCAT ( profileSalary . posNoAbb , profileSalary . posNo ) LIKE :keyword )
OR ( CONCAT ( profileSalary . posNoAbb , " " , profileSalary . posNo ) LIKE :keyword )
OR ( profileSalary . posNo LIKE :keyword )
2025-10-02 18:42:14 +07:00
` ;
default :
2025-10-03 13:05:24 +07:00
return ` CONCAT( ${ type } .prefix, ${ type } .firstName, ' ', ${ type } .lastName) LIKE :keyword ` ;
2025-10-02 18:42:14 +07:00
}
}
2025-10-04 13:54:17 +07:00
async findOrgNodeParentAll ( node : number , nodeId : string ) : Promise < OrgParentName > {
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 */
2025-10-02 18:42:14 +07:00
async buildNodeCondition (
2025-10-04 13:54:17 +07:00
node : number ,
nodeId : string ,
2025-10-02 18:42:14 +07:00
isAll? : boolean ,
) : Promise < OrganizationCondition > {
2025-10-04 13:54:17 +07:00
// 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 ) ;
2026-05-22 13:33:22 +07:00
for ( let index = 0 ; index <= node ; index ++ ) {
const config = this . nodeConfigs [ 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 ;
}
}
2025-10-02 18:42:14 +07:00
2025-10-04 13:54:17 +07:00
return {
condition : nodeCondition ,
params ,
} ;
}
async getOrgNameFromId ( orgIds : {
root : string | null ;
child1 : string | null ;
child2 : string | null ;
child3 : string | null ;
child4 : string | null ;
} ) : Promise < OrgParentName > {
2026-05-22 13:33:22 +07:00
const [ rootName , child1 , child2 , child3 , child4 ] = await Promise . all ( [
orgIds . root
? this . orgRootRepository . findOne ( { where : { id : orgIds.root } , select : [ "orgRootName" ] } )
: Promise . resolve ( null ) ,
orgIds . child1
? this . child1Repository . findOne ( { where : { id : orgIds.child1 } , select : [ "orgChild1Name" ] } )
: Promise . resolve ( null ) ,
orgIds . child2
? this . child2Repository . findOne ( { where : { id : orgIds.child2 } , select : [ "orgChild2Name" ] } )
: Promise . resolve ( null ) ,
orgIds . child3
? this . child3Repository . findOne ( { where : { id : orgIds.child3 } , select : [ "orgChild3Name" ] } )
: Promise . resolve ( null ) ,
orgIds . child4
? this . child4Repository . findOne ( { where : { id : orgIds.child4 } , select : [ "orgChild4Name" ] } )
: Promise . resolve ( null ) ,
] ) ;
2025-10-04 13:54:17 +07:00
2026-05-22 13:33:22 +07:00
return {
orgRootName : rootName?.orgRootName ? ? null ,
orgChild1Name : child1?.orgChild1Name ? ? null ,
orgChild2Name : child2?.orgChild2Name ? ? null ,
orgChild3Name : child3?.orgChild3Name ? ? null ,
orgChild4Name : child4?.orgChild4Name ? ? null ,
} ;
2025-10-02 18:42:14 +07:00
}
2025-10-04 13:54:17 +07:00
/** สร้างเงื่อนไขการค้นหาตาม node และ nodeId และเช็คกับ permission */
2025-10-02 18:42:14 +07:00
async buildPermissionCondition (
2025-10-04 13:54:17 +07:00
_data : DataPermission ,
2025-10-02 18:42:14 +07:00
isAll? : boolean ,
) : Promise < OrganizationCondition > {
2025-10-04 13:54:17 +07:00
// Early return สำหรับ OWNER privilege
2026-02-20 11:46:46 +07:00
if ( _data . privilege === "OWNER" || _data . privilege === "PARENT" ) {
2025-10-04 13:54:17 +07:00
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
2025-10-02 18:42:14 +07:00
}
2026-05-22 13:33:22 +07:00
for ( let index = 0 ; index < this . nodeConfigs . length ; index ++ ) {
const config = this . nodeConfigs [ 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 ;
}
}
2025-10-04 13:54:17 +07:00
return {
condition : nodeCondition ,
params ,
} ;
2025-10-02 18:42:14 +07:00
}
2025-10-03 13:05:24 +07:00
/** แปลงข้อมูลลูกจ้างก่อน response */
2025-10-02 18:42:14 +07:00
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 ,
} ;
}
2025-10-03 13:05:24 +07:00
/** ค้นหาลูกจ้างที่พ้นจากราชการ */
2025-10-02 18:42:14 +07:00
async getLeaveEmployees (
request : RequestWithUser ,
2025-10-03 13:05:24 +07:00
filter : LeaveFilter ,
2025-10-02 18:42:14 +07:00
) : Promise < { data : any [ ] ; total : number } > {
const {
page ,
pageSize ,
searchField ,
searchKeyword = "" ,
posType ,
posLevel ,
isProbation ,
node ,
nodeId ,
isAll ,
retireType ,
sortBy = "profileEmployee.dateLeave" ,
sort ,
2025-10-04 13:54:17 +07:00
_data ,
2025-10-02 18:42:14 +07:00
} = filter ;
2026-05-22 13:33:22 +07:00
const t0 = Date . now ( ) ;
2025-10-03 13:05:24 +07:00
const searchQuery = this . buildSearchQuery ( searchField , "profileEmployee" ) ;
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// สร้าง base WHERE conditions แชร์ระหว่าง count/id/data query
const baseWhere = ( qb : any ) = > {
qb . where (
new Brackets ( ( qb2 ) = > {
qb2 . where ( "profileEmployee.isLeave = :isLeave" , { isLeave : true } ) . orWhere (
2025-10-02 18:42:14 +07:00
"profileEmployee.isRetirement = :isRetirement" ,
{ isRetirement : true } ,
) ;
} ) ,
)
2026-05-22 13:33:22 +07:00
. andWhere ( "profileEmployee.employeeClass LIKE :type" , { type : "PERM" } )
. andWhere (
new Brackets ( ( qb2 ) = > {
qb2 . orWhere ( searchKeyword && searchKeyword != "" ? searchQuery : "1=1" , {
keyword : ` % ${ searchKeyword } % ` ,
} ) ;
} ) ,
) ;
if ( posType ) {
qb . andWhere ( "posType.posTypeName LIKE :keyword1" , { keyword1 : ` ${ posType } ` } ) ;
}
if ( posLevel ) {
qb . andWhere (
"CONCAT(posType.posTypeShortName, ' ', posLevel.posLevelName) LIKE :keyword2" ,
{ keyword2 : ` ${ posLevel } ` } ,
) ;
}
if ( isProbation !== undefined && isProbation !== null ) {
qb . andWhere ( "profileEmployee.isProbation = :isProbation" , { isProbation } ) ;
}
if ( retireType ) {
qb . andWhere ( "profileEmployee.leaveType = :retireType" , { retireType } ) ;
}
} ;
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// Compute permission/node conditions เพียงครั้งเดียว
const conditions : { condition : string ; params : Record < string , any > } [ ] = [ ] ;
if ( _data . privilege !== "OWNER" && _data . privilege !== "PARENT" ) {
conditions . push ( await this . buildPermissionCondition ( _data , isAll ) ) ;
2025-10-02 18:42:14 +07:00
}
2026-05-22 13:33:22 +07:00
if ( node !== null && node !== undefined && nodeId ) {
conditions . push ( await this . buildNodeCondition ( node , nodeId , isAll ) ) ;
2025-10-02 18:42:14 +07:00
}
2026-05-22 13:33:22 +07:00
const applyConditions = ( qb : any ) = > {
for ( const cond of conditions ) {
qb . andWhere ( cond . condition , cond . params ) ;
}
} ;
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// console.log(`[ProfileLeaveService] getLeaveEmployees conditions took ${Date.now() - t0}ms`);
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// สร้าง salary EXISTS filter (ใช้ซ้ำทั้ง step1, step2)
const applySalaryFilter = ( qb : any ) = > {
if ( conditions . length > 0 ) {
let existsCond = "profileSalary.positionName != :notRetire" ;
const existsParams : Record < string , any > = { notRetire : "เกษียณอายุราชการ" } ;
for ( const cond of conditions ) {
existsCond += ` AND ${ cond . condition } ` ;
Object . assign ( existsParams , cond . params ) ;
}
qb . andWhere (
` EXISTS (SELECT 1 FROM profileSalary WHERE profileEmployeeId = profileEmployee.id AND ${ existsCond } AND profileSalary. \` order \` = (SELECT MAX(ps. \` order \` ) FROM profileSalary ps WHERE ps.profileEmployeeId = profileEmployee.id AND ps.positionName != :notRetire2)) ` ,
{ . . . existsParams , notRetire2 : "เกษียณอายุราชการ" }
) ;
}
} ;
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// Step 1: Count query
const countQb = this . profileEmployeeRepo
. createQueryBuilder ( "profileEmployee" )
. leftJoinAndSelect ( "profileEmployee.posLevel" , "posLevel" )
. leftJoinAndSelect ( "profileEmployee.posType" , "posType" ) ;
baseWhere ( countQb ) ;
applySalaryFilter ( countQb ) ;
const total = await countQb . getCount ( ) ;
2025-10-04 13:54:17 +07:00
2026-05-22 13:33:22 +07:00
// console.log(`[ProfileLeaveService] getLeaveEmployees count took ${Date.now() - t0}ms, total=${total}`);
2025-10-04 13:54:17 +07:00
2026-05-22 13:33:22 +07:00
// Step 2: ดึงเฉพาะ profileEmployee IDs ที่ผ่านเงื่อนไข
const idQb = this . profileEmployeeRepo
. createQueryBuilder ( "profileEmployee" )
. select ( [ "profileEmployee.id" ] )
. leftJoin ( "profileEmployee.posLevel" , "posLevel" )
. leftJoin ( "profileEmployee.posType" , "posType" ) ;
baseWhere ( idQb ) ;
applySalaryFilter ( idQb ) ;
idQb . orderBy ( sortBy , sort ) . skip ( ( page - 1 ) * pageSize ) . take ( pageSize ) ;
const rawIds = await idQb . getRawMany ( ) ;
const employeeIds = rawIds . map ( ( r ) = > r . profileEmployee_id ) ;
// console.log(`[ProfileLeaveService] getLeaveEmployees ids took ${Date.now() - t0}ms, ids=${employeeIds.length}`);
if ( employeeIds . length === 0 ) {
return { data : [ ] , total } ;
2025-10-04 13:54:17 +07:00
}
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// Step 3: Load full data โดยไม่ JOIN salary
const records = await this . profileEmployeeRepo . find ( {
where : { id : In ( employeeIds ) } ,
relations : [ "posLevel" , "posType" , "profileEmployeeEmployment" ] ,
order : { [ sortBy . split ( "." ) [ 1 ] ] : sort } as any ,
} ) ;
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// Step 4: Load salary เฉพาะ row ที่มี order สูงสุดต่อ profileEmployeeId (INNER JOIN + GROUP BY)
const salaries = await this . profileSalaryRepo
. createQueryBuilder ( "ps" )
. innerJoin (
( subQuery ) = >
subQuery
. select ( "ps2.profileEmployeeId" , "pid" )
. addSelect ( "MAX(ps2.order)" , "maxOrd" )
. from ( ProfileSalary , "ps2" )
. where ( "ps2.profileEmployeeId IN (:...employeeIds)" , { employeeIds } )
. andWhere ( "ps2.positionName != :notRetire" , { notRetire : "เกษียณอายุราชการ" } )
. groupBy ( "ps2.profileEmployeeId" ) ,
"latest" ,
"latest.pid = ps.profileEmployeeId AND ps.order = latest.maxOrd"
)
. getMany ( ) ;
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// สร้าง map: profileEmployeeId → salary ที่มี order สูงสุด
const salaryMap = new Map < string , ProfileSalary > ( ) ;
for ( const s of salaries ) {
salaryMap . set ( s . profileEmployeeId , s ) ;
}
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// แปลงข้อมูลพร้อม salary
const data = records . map ( ( record ) = > {
const salary = salaryMap . get ( record . id ) ;
if ( salary ) {
( record as any ) . profileSalary = [ salary ] ;
}
return this . transformEmployeeData ( record ) ;
} ) ;
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// console.log(`[ProfileLeaveService] getLeaveEmployees total took ${Date.now() - t0}ms, total=${total}`);
2025-10-03 13:05:24 +07:00
return { data , total } ;
}
/ * *
* แ ป ล ง ข ้ อ ม ู ล ล ู ก จ ้ า ง ก ่ อ น response
* /
2025-10-04 13:54:17 +07:00
transformOfficerData ( employee : Profile ) {
2025-10-03 13:05:24 +07:00
// ตรวจสอบว่า 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 ,
2025-10-04 13:54:17 +07:00
_data ,
2025-10-03 13:05:24 +07:00
} = filter ;
2026-05-22 13:33:22 +07:00
const t0 = Date . now ( ) ;
2025-10-03 13:05:24 +07:00
const searchQuery = this . buildSearchQuery ( searchField ) ;
2026-05-22 13:33:22 +07:00
// สร้าง base WHERE conditions แชร์ระหว่าง count/id/data query
const baseWhere = ( qb : any ) = > {
qb . where (
new Brackets ( ( qb2 ) = > {
qb2 . where ( "profile.isLeave = :isLeave" , { isLeave : true } ) . orWhere (
2025-10-03 13:05:24 +07:00
"profile.isRetirement = :isRetirement" ,
{ isRetirement : true } ,
) ;
} ) ,
2026-05-22 13:33:22 +07:00
) . andWhere (
new Brackets ( ( qb2 ) = > {
qb2 . orWhere ( searchKeyword && searchKeyword != "" ? searchQuery : "1=1" , {
2025-10-04 13:54:17 +07:00
keyword : ` % ${ searchKeyword } % ` ,
} ) ;
2025-10-03 13:05:24 +07:00
} ) ,
) ;
2026-05-22 13:33:22 +07:00
if ( posType ) {
qb . andWhere ( "posType.posTypeName LIKE :keyword1" , { keyword1 : ` ${ posType } ` } ) ;
}
if ( posLevel ) {
qb . andWhere ( "posLevel.posLevelName LIKE :keyword2" , { keyword2 : ` ${ posLevel } ` } ) ;
}
if ( isProbation !== undefined && isProbation !== null ) {
qb . andWhere ( "profile.isProbation = :isProbation" , { isProbation } ) ;
}
if ( retireType ) {
qb . andWhere ( "profile.leaveType = :retireType" , { retireType } ) ;
}
} ;
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// Compute permission/node conditions เพียงครั้งเดียว
const conditions : { condition : string ; params : Record < string , any > } [ ] = [ ] ;
if ( _data . privilege !== "OWNER" && _data . privilege !== "PARENT" ) {
conditions . push ( await this . buildPermissionCondition ( _data , isAll ) ) ;
2025-10-03 13:05:24 +07:00
}
2026-05-22 13:33:22 +07:00
if ( node !== null && node !== undefined && nodeId ) {
conditions . push ( await this . buildNodeCondition ( node , nodeId , isAll ) ) ;
2025-10-03 13:05:24 +07:00
}
2026-05-22 13:33:22 +07:00
const applyConditions = ( qb : any ) = > {
for ( const cond of conditions ) {
qb . andWhere ( cond . condition , cond . params ) ;
}
} ;
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// console.log(`[ProfileLeaveService] getLeaveOfficer conditions took ${Date.now() - t0}ms`);
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// สร้าง salary EXISTS filter (ใช้ซ้ำทั้ง step1, step2)
const applySalaryFilter = ( qb : any ) = > {
if ( conditions . length > 0 ) {
let existsCond = "profileSalary.positionName != :notRetire" ;
const existsParams : Record < string , any > = { notRetire : "เกษียณอายุราชการ" } ;
for ( const cond of conditions ) {
existsCond += ` AND ${ cond . condition } ` ;
Object . assign ( existsParams , cond . params ) ;
}
qb . andWhere (
` EXISTS (SELECT 1 FROM profileSalary WHERE profileId = profile.id AND ${ existsCond } AND profileSalary. \` order \` = (SELECT MAX(ps. \` order \` ) FROM profileSalary ps WHERE ps.profileId = profile.id AND ps.positionName != :notRetire2)) ` ,
{ . . . existsParams , notRetire2 : "เกษียณอายุราชการ" }
) ;
2025-10-04 13:54:17 +07:00
}
2026-05-22 13:33:22 +07:00
} ;
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// Step 1: Count query
const countQb = this . profileRepo
. createQueryBuilder ( "profile" )
. leftJoinAndSelect ( "profile.posLevel" , "posLevel" )
. leftJoinAndSelect ( "profile.posType" , "posType" ) ;
baseWhere ( countQb ) ;
applySalaryFilter ( countQb ) ;
const total = await countQb . getCount ( ) ;
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// console.log(`[ProfileLeaveService] getLeaveOfficer count took ${Date.now() - t0}ms, total=${total}`);
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// Step 2: ดึงเฉพาะ profile IDs ที่ผ่านเงื่อนไข
const idQb = this . profileRepo
. createQueryBuilder ( "profile" )
. select ( [ "profile.id" ] )
. leftJoin ( "profile.posLevel" , "posLevel" )
. leftJoin ( "profile.posType" , "posType" ) ;
baseWhere ( idQb ) ;
applySalaryFilter ( idQb ) ;
idQb . orderBy ( sortBy , sort ) . skip ( ( page - 1 ) * pageSize ) . take ( pageSize ) ;
const rawIds = await idQb . getRawMany ( ) ;
const profileIds = rawIds . map ( ( r ) = > r . profile_id ) ;
// console.log(`[ProfileLeaveService] getLeaveOfficer ids took ${Date.now() - t0}ms, ids=${profileIds.length}`);
if ( profileIds . length === 0 ) {
return { data : [ ] , total } ;
}
2025-10-03 13:05:24 +07:00
2026-05-22 13:33:22 +07:00
// Step 3: Load full data โดยไม่ JOIN salary
const records = await this . profileRepo . find ( {
where : { id : In ( profileIds ) } ,
relations : [ "posLevel" , "posType" ] ,
order : { [ sortBy . split ( "." ) [ 1 ] ] : sort } as any ,
} ) ;
// console.log(`[ProfileLeaveService] getLeaveOfficer step3 (load profiles) took ${Date.now() - t0}ms`);
// Step 4: Load salary เฉพาะ row ที่มี order สูงสุดต่อ profileId (INNER JOIN + GROUP BY)
const salaries = await this . profileSalaryRepo
. createQueryBuilder ( "ps" )
. innerJoin (
( subQuery ) = >
subQuery
. select ( "ps2.profileId" , "pid" )
. addSelect ( "MAX(ps2.order)" , "maxOrd" )
. from ( ProfileSalary , "ps2" )
. where ( "ps2.profileId IN (:...profileIds)" , { profileIds } )
. andWhere ( "ps2.positionName != :notRetire" , { notRetire : "เกษียณอายุราชการ" } )
. groupBy ( "ps2.profileId" ) ,
"latest" ,
"latest.pid = ps.profileId AND ps.order = latest.maxOrd"
)
. getMany ( ) ;
// console.log(`[ProfileLeaveService] getLeaveOfficer step4 (load salaries) took ${Date.now() - t0}ms, salary rows=${salaries.length}`);
// สร้าง map: profileId → salary ที่มี order สูงสุด
const salaryMap = new Map < string , ProfileSalary > ( ) ;
for ( const s of salaries ) {
salaryMap . set ( s . profileId , s ) ;
}
// แปลงข้อมูลพร้อม salary
const data = records . map ( ( record ) = > {
const salary = salaryMap . get ( record . id ) ;
if ( salary ) {
( record as any ) . profileSalary = [ salary ] ;
}
return this . transformOfficerData ( record ) ;
} ) ;
2025-10-02 18:42:14 +07:00
2026-05-22 13:33:22 +07:00
// console.log(`[ProfileLeaveService] getLeaveOfficer total took ${Date.now() - t0}ms, total=${total}`);
2025-10-02 18:42:14 +07:00
return { data , total } ;
}
}