817 lines
29 KiB
TypeScript
817 lines
29 KiB
TypeScript
import { AppDataSource } from "../database/data-source";
|
|
import { Profile } from "./../entities/Profile";
|
|
import { ProfileEmployee } from "../entities/ProfileEmployee";
|
|
import { ProfileSalary } from "./../entities/ProfileSalary";
|
|
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, In, 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<string, any>;
|
|
}
|
|
|
|
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;
|
|
}
|
|
export class ProfileLeaveService {
|
|
private profileEmployeeRepo: Repository<ProfileEmployee>;
|
|
private profileRepo: Repository<Profile>;
|
|
private profileSalaryRepo: Repository<ProfileSalary>;
|
|
private orgRootRepository: Repository<OrgRoot>;
|
|
private child1Repository: Repository<OrgChild1>;
|
|
private child2Repository: Repository<OrgChild2>;
|
|
private child3Repository: Repository<OrgChild3>;
|
|
private child4Repository: Repository<OrgChild4>;
|
|
private readonly nodeConfigs: NodeConfig[];
|
|
|
|
constructor() {
|
|
this.profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
|
|
this.profileRepo = AppDataSource.getRepository(Profile);
|
|
this.profileSalaryRepo = AppDataSource.getRepository(ProfileSalary);
|
|
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<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 */
|
|
async buildNodeCondition(
|
|
node: number,
|
|
nodeId: string,
|
|
isAll?: boolean,
|
|
): Promise<OrganizationCondition> {
|
|
// 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);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
return {
|
|
condition: nodeCondition,
|
|
params,
|
|
};
|
|
}
|
|
|
|
async getOrgNameFromId(orgIds: {
|
|
root: string | null;
|
|
child1: string | null;
|
|
child2: string | null;
|
|
child3: string | null;
|
|
child4: string | null;
|
|
}): Promise<OrgParentName> {
|
|
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),
|
|
]);
|
|
|
|
return {
|
|
orgRootName: rootName?.orgRootName ?? null,
|
|
orgChild1Name: child1?.orgChild1Name ?? null,
|
|
orgChild2Name: child2?.orgChild2Name ?? null,
|
|
orgChild3Name: child3?.orgChild3Name ?? null,
|
|
orgChild4Name: child4?.orgChild4Name ?? null,
|
|
};
|
|
}
|
|
|
|
/** สร้างเงื่อนไขการค้นหาตาม node และ nodeId และเช็คกับ permission */
|
|
async buildPermissionCondition(
|
|
_data: DataPermission,
|
|
isAll?: boolean,
|
|
): Promise<OrganizationCondition> {
|
|
// Early return สำหรับ OWNER privilege
|
|
if (_data.privilege === "OWNER" || _data.privilege === "PARENT") {
|
|
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
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 t0 = Date.now();
|
|
const searchQuery = this.buildSearchQuery(searchField, "profileEmployee");
|
|
|
|
// สร้าง base WHERE conditions แชร์ระหว่าง count/id/data query
|
|
const baseWhere = (qb: any) => {
|
|
qb.where(
|
|
new Brackets((qb2) => {
|
|
qb2.where("profileEmployee.isLeave = :isLeave", { isLeave: true }).orWhere(
|
|
"profileEmployee.isRetirement = :isRetirement",
|
|
{ isRetirement: true },
|
|
);
|
|
}),
|
|
)
|
|
.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 });
|
|
}
|
|
};
|
|
|
|
// 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));
|
|
}
|
|
if (node !== null && node !== undefined && nodeId) {
|
|
conditions.push(await this.buildNodeCondition(node, nodeId, isAll));
|
|
}
|
|
const applyConditions = (qb: any) => {
|
|
for (const cond of conditions) {
|
|
qb.andWhere(cond.condition, cond.params);
|
|
}
|
|
};
|
|
|
|
// console.log(`[ProfileLeaveService] getLeaveEmployees conditions took ${Date.now() - t0}ms`);
|
|
|
|
// สร้าง 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: "เกษียณอายุราชการ" }
|
|
);
|
|
}
|
|
};
|
|
|
|
// 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();
|
|
|
|
// console.log(`[ProfileLeaveService] getLeaveEmployees count took ${Date.now() - t0}ms, total=${total}`);
|
|
|
|
// 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 };
|
|
}
|
|
|
|
// 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,
|
|
});
|
|
|
|
// 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();
|
|
|
|
// สร้าง map: profileEmployeeId → salary ที่มี order สูงสุด
|
|
const salaryMap = new Map<string, ProfileSalary>();
|
|
for (const s of salaries) {
|
|
salaryMap.set(s.profileEmployeeId, s);
|
|
}
|
|
|
|
// แปลงข้อมูลพร้อม salary
|
|
const data = records.map((record) => {
|
|
const salary = salaryMap.get(record.id);
|
|
if (salary) {
|
|
(record as any).profileSalary = [salary];
|
|
}
|
|
return this.transformEmployeeData(record);
|
|
});
|
|
|
|
// console.log(`[ProfileLeaveService] getLeaveEmployees total took ${Date.now() - t0}ms, total=${total}`);
|
|
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 t0 = Date.now();
|
|
const searchQuery = this.buildSearchQuery(searchField);
|
|
|
|
// สร้าง base WHERE conditions แชร์ระหว่าง count/id/data query
|
|
const baseWhere = (qb: any) => {
|
|
qb.where(
|
|
new Brackets((qb2) => {
|
|
qb2.where("profile.isLeave = :isLeave", { isLeave: true }).orWhere(
|
|
"profile.isRetirement = :isRetirement",
|
|
{ isRetirement: true },
|
|
);
|
|
}),
|
|
).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("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 });
|
|
}
|
|
};
|
|
|
|
// 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));
|
|
}
|
|
if (node !== null && node !== undefined && nodeId) {
|
|
conditions.push(await this.buildNodeCondition(node, nodeId, isAll));
|
|
}
|
|
const applyConditions = (qb: any) => {
|
|
for (const cond of conditions) {
|
|
qb.andWhere(cond.condition, cond.params);
|
|
}
|
|
};
|
|
|
|
// console.log(`[ProfileLeaveService] getLeaveOfficer conditions took ${Date.now() - t0}ms`);
|
|
|
|
// สร้าง 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: "เกษียณอายุราชการ" }
|
|
);
|
|
}
|
|
};
|
|
|
|
// 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();
|
|
|
|
// console.log(`[ProfileLeaveService] getLeaveOfficer count took ${Date.now() - t0}ms, total=${total}`);
|
|
|
|
// 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 };
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
|
|
// console.log(`[ProfileLeaveService] getLeaveOfficer total took ${Date.now() - t0}ms, total=${total}`);
|
|
return { data, total };
|
|
}
|
|
}
|