hrms-api-org/src/services/OrganizationService.ts
2026-02-19 15:41:49 +07:00

407 lines
15 KiB
TypeScript

import { AppDataSource } from "../database/data-source";
import { PosMaster } from "../entities/PosMaster";
// Type definitions for position counts
export interface PositionCountData {
totalCount: number;
currentVacantCount: number;
nextVacantCount: number;
}
export interface PositionCountsByNode {
orgRootMap: Map<string, PositionCountData>;
orgChild1Map: Map<string, PositionCountData>;
orgChild2Map: Map<string, PositionCountData>;
orgChild3Map: Map<string, PositionCountData>;
orgChild4Map: Map<string, PositionCountData>;
}
// Optimized function using SQL aggregation with GROUP BY
// This is much more efficient than loading all records into memory
export async function getPositionCountsAggregated(orgRevisionId: string): Promise<PositionCountsByNode> {
const posRepo = AppDataSource.getRepository(PosMaster);
// Helper to build map from query results
const buildMap = (results: any[]) => {
const map = new Map<string, PositionCountData>();
for (const row of results) {
if (row.nodeId) {
map.set(row.nodeId, {
totalCount: parseInt(row.totalCount) || 0,
currentVacantCount: parseInt(row.currentVacantCount) || 0,
nextVacantCount: parseInt(row.nextVacantCount) || 0,
});
}
}
return map;
};
// Execute all aggregation queries in parallel
const [
orgRootResults,
orgChild1Results,
orgChild2Results,
orgChild3Results,
orgChild4Results,
] = await Promise.all([
// Level 0: orgRoot
posRepo
.createQueryBuilder("pos")
.select("pos.orgRootId", "nodeId")
.addSelect("COUNT(*)", "totalCount")
.addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount")
.addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount")
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
.andWhere("pos.orgRootId IS NOT NULL")
.groupBy("pos.orgRootId")
.getRawMany(),
// Level 1: orgChild1
posRepo
.createQueryBuilder("pos")
.select("pos.orgChild1Id", "nodeId")
.addSelect("COUNT(*)", "totalCount")
.addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount")
.addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount")
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
.andWhere("pos.orgChild1Id IS NOT NULL")
.groupBy("pos.orgChild1Id")
.getRawMany(),
// Level 2: orgChild2
posRepo
.createQueryBuilder("pos")
.select("pos.orgChild2Id", "nodeId")
.addSelect("COUNT(*)", "totalCount")
.addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount")
.addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount")
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
.andWhere("pos.orgChild2Id IS NOT NULL")
.groupBy("pos.orgChild2Id")
.getRawMany(),
// Level 3: orgChild3
posRepo
.createQueryBuilder("pos")
.select("pos.orgChild3Id", "nodeId")
.addSelect("COUNT(*)", "totalCount")
.addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount")
.addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount")
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
.andWhere("pos.orgChild3Id IS NOT NULL")
.groupBy("pos.orgChild3Id")
.getRawMany(),
// Level 4: orgChild4
posRepo
.createQueryBuilder("pos")
.select("pos.orgChild4Id", "nodeId")
.addSelect("COUNT(*)", "totalCount")
.addSelect("SUM(CASE WHEN pos.current_holderId IS NULL OR pos.current_holderId = '' THEN 1 ELSE 0 END)", "currentVacantCount")
.addSelect("SUM(CASE WHEN pos.next_holderId IS NULL OR pos.next_holderId = '' THEN 1 ELSE 0 END)", "nextVacantCount")
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
.andWhere("pos.orgChild4Id IS NOT NULL")
.groupBy("pos.orgChild4Id")
.getRawMany(),
]);
return {
orgRootMap: buildMap(orgRootResults),
orgChild1Map: buildMap(orgChild1Results),
orgChild2Map: buildMap(orgChild2Results),
orgChild3Map: buildMap(orgChild3Results),
orgChild4Map: buildMap(orgChild4Results),
};
}
// Legacy function - kept for backward compatibility
// DEPRECATED: Use getPositionCountsAggregated instead
export async function getPositionCounts(orgRevisionId: string) {
// Query all posMaster data for this revision with aggregation
const rawData = await AppDataSource.getRepository(PosMaster)
.createQueryBuilder("pos")
.select([
"pos.orgRootId",
"pos.orgChild1Id",
"pos.orgChild2Id",
"pos.orgChild3Id",
"pos.orgChild4Id",
"pos.current_holderId",
"pos.next_holderId",
])
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
.getMany();
// Helper to check if value is null or empty string
const isNull = (val: any) => val === null || val === "";
// Build maps for each level
const orgRootMap = new Map<string, any>();
const orgChild1Map = new Map<string, any>();
const orgChild2Map = new Map<string, any>();
const orgChild3Map = new Map<string, any>();
const orgChild4Map = new Map<string, any>();
// Nested maps for root positions (positions at specific levels)
const rootPosMap = new Map<string, any>();
for (const pos of rawData) {
const orgRootId = pos.orgRootId || "NULL";
const orgChild1Id = pos.orgChild1Id || "NULL";
const orgChild2Id = pos.orgChild2Id || "NULL";
const orgChild3Id = pos.orgChild3Id || "NULL";
const orgChild4Id = pos.orgChild4Id || "NULL";
// Level 0 (orgRoot) counts
if (!orgRootMap.has(orgRootId)) {
orgRootMap.set(orgRootId, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const rootCounts = orgRootMap.get(orgRootId);
rootCounts.totalPosition++;
if (!isNull(pos.current_holderId)) rootCounts.totalPositionCurrentUse++;
else rootCounts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) rootCounts.totalPositionNextUse++;
else rootCounts.totalPositionNextVacant++;
// Level 1 (orgChild1) counts
if (!isNull(pos.orgChild1Id)) {
if (!orgChild1Map.has(orgChild1Id)) {
orgChild1Map.set(orgChild1Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child1Counts = orgChild1Map.get(orgChild1Id);
child1Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child1Counts.totalPositionCurrentUse++;
else child1Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child1Counts.totalPositionNextUse++;
else child1Counts.totalPositionNextVacant++;
}
// Level 2 (orgChild2) counts
if (!isNull(pos.orgChild2Id)) {
if (!orgChild2Map.has(orgChild2Id)) {
orgChild2Map.set(orgChild2Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child2Counts = orgChild2Map.get(orgChild2Id);
child2Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child2Counts.totalPositionCurrentUse++;
else child2Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child2Counts.totalPositionNextUse++;
else child2Counts.totalPositionNextVacant++;
}
// Level 3 (orgChild3) counts
if (!isNull(pos.orgChild3Id)) {
if (!orgChild3Map.has(orgChild3Id)) {
orgChild3Map.set(orgChild3Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child3Counts = orgChild3Map.get(orgChild3Id);
child3Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child3Counts.totalPositionCurrentUse++;
else child3Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child3Counts.totalPositionNextUse++;
else child3Counts.totalPositionNextVacant++;
}
// Level 4 (orgChild4) counts
if (!isNull(pos.orgChild4Id)) {
if (!orgChild4Map.has(orgChild4Id)) {
orgChild4Map.set(orgChild4Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child4Counts = orgChild4Map.get(orgChild4Id);
child4Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child4Counts.totalPositionCurrentUse++;
else child4Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child4Counts.totalPositionNextUse++;
else child4Counts.totalPositionNextVacant++;
}
// Root position counts (positions at specific hierarchy levels)
// For orgRoot level (all children are null)
const rootLevelKey = orgRootId;
if (!rootPosMap.has(rootLevelKey)) {
rootPosMap.set(rootLevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild1Id) && isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
const rootLevelCounts = rootPosMap.get(rootLevelKey);
rootLevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) rootLevelCounts.totalRootPositionCurrentUse++;
else rootLevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) rootLevelCounts.totalRootPositionNextUse++;
else rootLevelCounts.totalRootPositionNextVacant++;
}
// For orgChild1 level
if (!isNull(pos.orgChild1Id)) {
const child1LevelKey = `${orgRootId}-${pos.orgChild1Id}`;
if (!rootPosMap.has(child1LevelKey)) {
rootPosMap.set(child1LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
const child1LevelCounts = rootPosMap.get(child1LevelKey);
child1LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child1LevelCounts.totalRootPositionCurrentUse++;
else child1LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child1LevelCounts.totalRootPositionNextUse++;
else child1LevelCounts.totalRootPositionNextVacant++;
}
}
// For orgChild2 level
if (!isNull(pos.orgChild2Id)) {
const child2LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}`;
if (!rootPosMap.has(child2LevelKey)) {
rootPosMap.set(child2LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
const child2LevelCounts = rootPosMap.get(child2LevelKey);
child2LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child2LevelCounts.totalRootPositionCurrentUse++;
else child2LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child2LevelCounts.totalRootPositionNextUse++;
else child2LevelCounts.totalRootPositionNextVacant++;
}
}
// For orgChild3 level
if (!isNull(pos.orgChild3Id)) {
const child3LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}`;
if (!rootPosMap.has(child3LevelKey)) {
rootPosMap.set(child3LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild4Id)) {
const child3LevelCounts = rootPosMap.get(child3LevelKey);
child3LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child3LevelCounts.totalRootPositionCurrentUse++;
else child3LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child3LevelCounts.totalRootPositionNextUse++;
else child3LevelCounts.totalRootPositionNextVacant++;
}
}
// For orgChild4 level
if (!isNull(pos.orgChild4Id)) {
const child4LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}-${pos.orgChild4Id}`;
if (!rootPosMap.has(child4LevelKey)) {
rootPosMap.set(child4LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
const child4LevelCounts = rootPosMap.get(child4LevelKey);
child4LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child4LevelCounts.totalRootPositionCurrentUse++;
else child4LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child4LevelCounts.totalRootPositionNextUse++;
else child4LevelCounts.totalRootPositionNextVacant++;
}
}
return {
orgRootMap,
orgChild1Map,
orgChild2Map,
orgChild3Map,
orgChild4Map,
rootPosMap,
};
}
// Helper function to get counts from maps with defaults
export function getCounts(map: Map<string, any>, key: string) {
return (
map.get(key) || {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
}
);
}
// Helper function to get root position counts
export function getRootCounts(map: Map<string, any>, key: string) {
return (
map.get(key) || {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
}
);
}
// Helper function to get position counts from aggregated maps with defaults
export function getPositionCount(
map: Map<string, PositionCountData>,
key: string | null | undefined
): PositionCountData {
if (!key) return { totalCount: 0, currentVacantCount: 0, nextVacantCount: 0 };
return (
map.get(key) || {
totalCount: 0,
currentVacantCount: 0,
nextVacantCount: 0,
}
);
}