hrms-api-org/src/services/ProfileLeaveService.ts

743 lines
24 KiB
TypeScript
Raw Normal View History

import { AppDataSource } from "../database/data-source";
2025-10-03 13:05:24 +07:00
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<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 {
2025-10-03 13:05:24 +07:00
private profileEmployeeRepo: Repository<ProfileEmployee>;
private profileRepo: Repository<Profile>;
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() {
2025-10-03 13:05:24 +07:00
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",
},
];
}
2025-10-03 13:05:24 +07:00
/** สร้าง query สำหรับการค้นหาตามฟิลด์ต่างๆ */
buildSearchQuery(searchField?: string, type: string = "profile"): string {
switch (searchField) {
case "citizenId":
2025-10-03 13:05:24 +07:00
return `${type}.citizenId LIKE :keyword`;
case "position":
2025-10-03 13:05:24 +07:00
return `${type}.position LIKE :keyword`;
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)
`;
default:
2025-10-03 13:05:24 +07:00
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);
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<OrgParentName> {
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<OrganizationCondition> {
// Early return สำหรับ OWNER privilege
2026-02-20 11:46:46 +07:00
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
}
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,
};
}
2025-10-03 13:05:24 +07:00
/** แปลงข้อมูลลูกจ้างก่อน 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,
};
}
2025-10-03 13:05:24 +07:00
/** ค้นหาลูกจ้างที่พ้นจากราชการ */
async getLeaveEmployees(
request: RequestWithUser,
2025-10-03 13:05:24 +07:00
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;
2025-10-03 13:05:24 +07:00
const searchQuery = this.buildSearchQuery(searchField, "profileEmployee");
// สร้าง main query - เปลี่ยนจาก leftJoinAndSelect เป็น leftJoin สำหรับ profileSalary
2025-10-03 13:05:24 +07:00
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 },
);
}),
)
2025-10-03 13:05:24 +07:00
.andWhere("profileEmployee.employeeClass LIKE :type", { type: "PERM" })
.andWhere(
new Brackets((qb) => {
qb.orWhere(searchKeyword && searchKeyword != "" ? searchQuery : "1=1", {
keyword: `%${searchKeyword}%`,
});
2025-10-03 13:05:24 +07:00
}),
);
// เพิ่มเงื่อนไขการค้นหา
if (posType) {
queryBuilder.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` });
}
if (posLevel) {
queryBuilder.andWhere(
2025-10-03 13:05:24 +07:00
"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);
2026-02-20 11:46:46 +07:00
if (_data.privilege !== "OWNER" && _data.privilege !== "PARENT") {
queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params);
}
}
2025-10-03 13:05:24 +07:00
// เพิ่ม 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) {
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,
_data,
2025-10-03 13:05:24 +07:00
} = 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}%`,
});
2025-10-03 13:05:24 +07:00
}),
);
if (posType) {
queryBuilder.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` });
}
if (posLevel) {
queryBuilder.andWhere("posLevel.posLevelName LIKE :keyword2", { keyword2: `${posLevel}` });
2025-10-03 13:05:24 +07:00
}
if (isProbation) {
2025-10-03 13:05:24 +07:00
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);
2026-02-20 11:46:46 +07:00
if (_data.privilege !== "OWNER" && _data.privilege !== "PARENT") {
queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params);
}
}
// เพิ่ม sorting และ pagination
queryBuilder
.orderBy(sortBy, sort)
.skip((page - 1) * pageSize)
.take(pageSize);
const [records, total] = await queryBuilder.getManyAndCount();
2025-10-03 13:05:24 +07:00
// print query for debug
// console.log("SQL Query:", queryBuilder.getSql());
const data = await Promise.all(
2025-10-03 13:05:24 +07:00
records.map((record) => Promise.resolve(this.transformOfficerData(record))),
);
return { data, total };
}
}