diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 97839425..2e5af5b9 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -3353,7 +3353,13 @@ export class OrganizationController extends Controller { "orgChild3", "orgChild4", ], - order: { posMasterOrder: "ASC" }, + order: { + orgRoot: { + orgRootOrder: "ASC", + }, + posMasterOrder: "ASC", + posMasterNo: "ASC", + }, }); // แยกข้อมูลด้วย JavaScript แทนการใช้ database queries หลาย ๆ ครั้ง @@ -3883,6 +3889,327 @@ export class OrganizationController extends Controller { } } + /** + * API Organizational Chart + * + * @summary Organizational Chart + * + * @param {string} revisionId Id revison + */ + @Get("org-chart/{revisionId}/{rootId}") + async orgChartByRoot(@Path() revisionId: string, @Path() rootId: string) { + // get revision data + const data = await this.orgRevisionRepository.findOne({ where: { id: revisionId } }); + if (!data) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้าง"); + } + + type OrgInfo = { shortName: string; name: string; id: string }; + + let fieldId: "current_holderId" | "next_holderId" = "current_holderId"; + let relations: string[] = + rootId === "root" + ? ["current_holder", "orgRoot"] + : ["current_holder", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"]; + if (data.orgRevisionIsCurrent === false && data.orgRevisionIsDraft === true) { + fieldId = "next_holderId"; + relations = + rootId === "root" + ? ["next_holder", "orgRoot"] + : ["next_holder", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"]; + } + + const where = + rootId === "root" + ? { orgRevisionId: data.id, posMasterNo: 1 } + : { orgRevisionId: data.id, orgRootId: rootId }; + + const allPosMasters = await this.posMasterRepository.find({ + where, + relations, + order: { + orgRoot: { orgRootOrder: "ASC" }, + posMasterOrder: "ASC", + posMasterNo: "ASC", + }, + }); + + // Split positions by level + const posMasterRoot = allPosMasters.filter((item) => item.orgChild1Id === null); + const posMasterChild1 = + rootId !== "root" + ? allPosMasters.filter((item) => item.orgChild2Id === null && item.orgChild1Id !== null) + : []; + const posMasterChild2 = + rootId !== "root" + ? allPosMasters.filter((item) => item.orgChild3Id === null && item.orgChild2Id !== null) + : []; + const posMasterChild3 = + rootId !== "root" + ? allPosMasters.filter((item) => item.orgChild4Id === null && item.orgChild3Id !== null) + : []; + const posMasterChild4 = + rootId !== "root" ? allPosMasters.filter((item) => item.orgChild4Id !== null) : []; + + // Find the minimum posMasterNo for each orgRootId + const minPosMasterNoByOrgRootId = new Map(); + posMasterRoot.forEach((x) => { + const orgRootId = x.orgRootId ?? ""; + const currentMin = minPosMasterNoByOrgRootId.get(orgRootId); + if ( + (currentMin === undefined || x.posMasterNo < currentMin) && + (x as any)[fieldId] != null && + x.isDirector + ) { + minPosMasterNoByOrgRootId.set(orgRootId, x.posMasterNo); + } + }); + + // Utility to group by key + function groupBy( + arr: T[], + keyFn: (item: T) => string | number | undefined | null, + ): Map { + const map = new Map(); + for (const item of arr) { + let key = keyFn(item); + if (key === undefined || key === null) key = ""; + key = String(key); + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(item); + } + return map; + } + + // Build lookup maps + const rootByOrgRootId = groupBy( + posMasterRoot.filter( + (item) => + ((item as any)[fieldId] && !item.isDirector) || + (item.isDirector && + minPosMasterNoByOrgRootId.get(item.orgRootId ?? "") !== item.posMasterNo), + ), + (item) => item.orgRootId ?? "", + ); + const child1ByOrgRootId = groupBy( + posMasterChild1.filter((item) => item.isDirector), + (item) => item.orgRootId ?? "", + ); + const child1ByOrgChild1Id = groupBy( + posMasterChild1.filter((item) => (item as any)[fieldId] && !item.isDirector), + (item) => item.orgChild1Id ?? "", + ); + const child2ByOrgChild1Id = groupBy( + posMasterChild2.filter((item) => item.isDirector), + (item) => item.orgChild1Id ?? "", + ); + const child2ByOrgChild2Id = groupBy( + posMasterChild2.filter((item) => (item as any)[fieldId] && !item.isDirector), + (item) => item.orgChild2Id ?? "", + ); + const child3ByOrgChild2Id = groupBy( + posMasterChild3.filter((item) => item.isDirector), + (item) => item.orgChild2Id ?? "", + ); + const child3ByOrgChild3Id = groupBy( + posMasterChild3.filter((item) => (item as any)[fieldId] && !item.isDirector), + (item) => item.orgChild3Id ?? "", + ); + const child4ByOrgChild3Id = groupBy( + posMasterChild4.filter((item) => item.isDirector), + (item) => item.orgChild3Id ?? "", + ); + + // Helper to create node + function createNode(item: any, level: number, orgInfo: OrgInfo): any { + const holder = fieldId === "current_holderId" ? item.current_holder : item.next_holder; + return { + level, + personID: holder?.id ?? "", + name: holder ? `${holder.firstName} ${holder.lastName}` : "ว่าง", + avatar: + holder?.avatar && holder?.avatarName ? `${holder.avatar}/${holder.avatarName}` : null, + positionName: holder?.position ?? "", + positionNum: `${orgInfo.shortName} ${item.posMasterNo}`, + positionNumInt: item.posMasterNo, + departmentName: orgInfo.name, + organizationId: orgInfo.id, + children: [], + }; + } + + // Recursive builder for children + function buildChildren(level: number, parent: any, orgInfo: OrgInfo): any[] { + if (level === 2) { + // Level 2: child1 non-directors and child2 directors + const children: any[] = []; + // Root non-directors + ( + rootByOrgRootId + .get(parent.orgRootId) + ?.filter( + (x00) => + !x00.isDirector || + (x00.isDirector && + minPosMasterNoByOrgRootId.get(x00.orgRootId ?? "") !== x00.posMasterNo), + ) || [] + ).forEach((x00) => { + children.push( + createNode(x00, 2, { + shortName: x00.orgRoot.orgRootShortName, + name: x00.orgRoot.orgRootName, + id: x00.orgRoot.id, + }), + ); + }); + if (rootId !== "root") { + // Child1 directors + (child1ByOrgRootId.get(parent.orgRootId) || []).forEach((x1) => { + const childLevel2 = buildChildren(3, x1, { + shortName: x1.orgChild1.orgChild1ShortName, + name: x1.orgChild1.orgChild1Name, + id: x1.orgChild1.id, + }); + const node = createNode(x1, 2, { + shortName: x1.orgChild1.orgChild1ShortName, + name: x1.orgChild1.orgChild1Name, + id: x1.orgChild1.id, + }); + node.children = childLevel2; + children.push(node); + }); + } + return children; + } else if (level === 3) { + // Level 3: child1 non-directors and child2 directors + const children: any[] = []; + (child1ByOrgChild1Id.get(parent.orgChild1Id) || []).forEach((x11) => { + children.push( + createNode(x11, 3, { + shortName: x11.orgChild1.orgChild1ShortName, + name: x11.orgChild1.orgChild1Name, + id: x11.orgChild1.id, + }), + ); + }); + (child2ByOrgChild1Id.get(parent.orgChild1Id) || []).forEach((x2) => { + const childLevel3 = buildChildren(4, x2, { + shortName: x2.orgChild2.orgChild2ShortName, + name: x2.orgChild2.orgChild2Name, + id: x2.orgChild2.id, + }); + const node = createNode(x2, 3, { + shortName: x2.orgChild2.orgChild2ShortName, + name: x2.orgChild2.orgChild2Name, + id: x2.orgChild2.id, + }); + node.children = childLevel3; + children.push(node); + }); + return children; + } else if (level === 4) { + // Level 4: child2 non-directors and child3 directors + const children: any[] = []; + (child2ByOrgChild2Id.get(parent.orgChild2Id) || []).forEach((x22) => { + children.push( + createNode(x22, 4, { + shortName: x22.orgChild2.orgChild2ShortName, + name: x22.orgChild2.orgChild2Name, + id: x22.orgChild2.id, + }), + ); + }); + (child3ByOrgChild2Id.get(parent.orgChild2Id) || []).forEach((x3) => { + const childLevel4 = buildChildren(5, x3, { + shortName: x3.orgChild3.orgChild3ShortName, + name: x3.orgChild3.orgChild3Name, + id: x3.orgChild3.id, + }); + const node = createNode(x3, 4, { + shortName: x3.orgChild3.orgChild3ShortName, + name: x3.orgChild3.orgChild3Name, + id: x3.orgChild3.id, + }); + node.children = childLevel4; + children.push(node); + }); + return children; + } else if (level === 5) { + // Level 5: child3 non-directors and child4 directors + const children: any[] = []; + (child3ByOrgChild3Id.get(parent.orgChild3Id) || []).forEach((x33) => { + children.push( + createNode(x33, 5, { + shortName: x33.orgChild3.orgChild3ShortName, + name: x33.orgChild3.orgChild3Name, + id: x33.orgChild3.id, + }), + ); + }); + (child4ByOrgChild3Id.get(parent.orgChild3Id) || []).forEach((x4) => { + // Level 5: child4 directors and their non-directors + const childLevel5: any[] = []; + posMasterChild4 + .filter((x) => !x.isDirector && (x as any)[fieldId] && x.orgChild3Id === x4.orgChild3Id) + .forEach((x44) => { + childLevel5.push( + createNode(x44, 5, { + shortName: x44.orgChild4.orgChild4ShortName, + name: x44.orgChild4.orgChild4Name, + id: x44.orgChild4.id, + }), + ); + }); + const node = createNode(x4, 4, { + shortName: x4.orgChild4.orgChild4ShortName, + name: x4.orgChild4.orgChild4Name, + id: x4.orgChild4.id, + }); + node.children = childLevel5; + children.push(node); + }); + return children; + } + return []; + } + + // Build root nodes + const formattedData = posMasterRoot + .filter( + (x: any) => + x.isDirector && minPosMasterNoByOrgRootId.get(x.orgRootId ?? "") === x.posMasterNo, + ) + .map((x0) => { + const rootNode = createNode(x0, 1, { + shortName: x0.orgRoot.orgRootShortName, + name: x0.orgRoot.orgRootName, + id: x0.orgRoot.id, + }); + rootNode.children = buildChildren(2, x0, { + shortName: x0.orgRoot.orgRootShortName, + name: x0.orgRoot.orgRootName, + id: x0.orgRoot.id, + }); + return rootNode; + }); + + const formattedData_ = + rootId === "root" + ? { + personID: "", + name: "", + avatar: "", + positionName: "", + positionNum: "", + positionNumInt: null, + departmentName: data.orgRevisionName, + organizationId: data.id, + children: formattedData, + } + : formattedData; + return new HttpSuccess([formattedData_]); + } + /** * API Organizational StructChart *