From 27f53b252d3700fe6e667dc58bd2b107642c7dc7 Mon Sep 17 00:00:00 2001 From: kittapath <> Date: Mon, 15 Sep 2025 22:32:13 +0700 Subject: [PATCH 1/5] use npm install --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 07492be..3c7b03d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app # Install app dependencies COPY package*.json ./ -RUN npm ci +RUN npm install # Copy source files and build the app COPY . . From 41c97e5f12f0d64fbd90e15cea2f65e4e9fb93d6 Mon Sep 17 00:00:00 2001 From: kittapath <> Date: Mon, 15 Sep 2025 22:50:17 +0700 Subject: [PATCH 2/5] RUN npm install --production --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3c7b03d..36fe569 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,7 +27,7 @@ COPY --from=build-stage /app/dist ./dist # Install only production dependencies COPY package*.json ./ -RUN npm ci --production +RUN npm install --production # Define the entrypoint and default command # If you have a custom entrypoint script From 16426f1d0bc22fb5db0fa8878ce6bdb1299a6fbc Mon Sep 17 00:00:00 2001 From: kittapath <> Date: Mon, 15 Sep 2025 22:56:44 +0700 Subject: [PATCH 3/5] test build --- docker/Dockerfile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 36fe569..a1d540a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,18 +16,21 @@ RUN npm run build # Production Stage FROM node:lts-alpine -ENV NODE_ENV production -USER node +ENV NODE_ENV=production # Create app directory WORKDIR /app +# Install only production dependencies as root first +COPY package*.json ./ +RUN npm install --production && npm cache clean --force + # Copy built app from build stage COPY --from=build-stage /app/dist ./dist -# Install only production dependencies -COPY package*.json ./ -RUN npm install --production +# Change ownership to node user and switch to node user +RUN chown -R node:node /app +USER node # Define the entrypoint and default command # If you have a custom entrypoint script From ec0aaf5b133a11ee4ae6ce6f7e34368f92cb311c Mon Sep 17 00:00:00 2001 From: kittapath <> Date: Tue, 16 Sep 2025 01:15:26 +0700 Subject: [PATCH 4/5] build --- docker/Dockerfile | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a1d540a..a9e52da 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # Build Stage -FROM node:lts-alpine AS build-stage +FROM node:22.17.1-alpine AS build-stage # Create app directory WORKDIR /app @@ -11,25 +11,24 @@ RUN npm install # Copy source files and build the app COPY . . +#RUN npm ci RUN npm run build # Production Stage FROM node:lts-alpine -ENV NODE_ENV=production +ENV NODE_ENV production # Create app directory WORKDIR /app -# Install only production dependencies as root first -COPY package*.json ./ -RUN npm install --production && npm cache clean --force - # Copy built app from build stage COPY --from=build-stage /app/dist ./dist -# Change ownership to node user and switch to node user -RUN chown -R node:node /app +# Install only production dependencies +COPY package*.json ./ +#RUN npm ci --production +RUN npm install USER node # Define the entrypoint and default command From ef1ac9cd0ff8e3424c1378a0e7111c78ef54def1 Mon Sep 17 00:00:00 2001 From: kittapath <> Date: Wed, 17 Sep 2025 00:55:34 +0700 Subject: [PATCH 5/5] snap salary --- src/controllers/SalaryPeriodController.ts | 1376 ++++++++++++--------- 1 file changed, 786 insertions(+), 590 deletions(-) diff --git a/src/controllers/SalaryPeriodController.ts b/src/controllers/SalaryPeriodController.ts index d51bc0d..d7525ac 100644 --- a/src/controllers/SalaryPeriodController.ts +++ b/src/controllers/SalaryPeriodController.ts @@ -2535,240 +2535,465 @@ export class SalaryPeriodController extends Controller { salaryPeriodId: string, @Request() request: RequestWithUser, ) { - try { - snapshot = snapshot.toLocaleUpperCase(); - const salaryPeriod = await this.salaryPeriodRepository.findOne({ - where: { id: salaryPeriodId }, - }); - if (!salaryPeriod) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบรอบการสร้างเงินเดือน"); + console.time("⏱ TOTAL SNAPSHOT PROCESSING TIME"); + + // Connection management for long-running process + const maxRetries = 3; + let retryCount = 0; + + while (retryCount < maxRetries) { + try { + snapshot = snapshot.toLocaleUpperCase(); + console.log( + `🚀 เริ่มต้น Snapshot: ${snapshot} สำหรับ SalaryPeriod: ${salaryPeriodId} (ครั้งที่ ${retryCount + 1})`, + ); + + // Check database connection + if (!AppDataSource.isInitialized) { + console.log("🔄 กำลังเชื่อมต่อฐานข้อมูลใหม่..."); + await AppDataSource.initialize(); + } + + return await this.performSnapshotOperation(snapshot, salaryPeriodId, request); + } catch (err) { + console.timeEnd("⏱ TOTAL SNAPSHOT PROCESSING TIME"); + const errorMessage = err instanceof Error ? err.message : String(err); + console.error( + `❌ Error in SnapshotSalary [${snapshot}] (ครั้งที่ ${retryCount + 1}):`, + errorMessage, + ); + + // Check if it's a connection error + if ( + errorMessage.includes("ECONNRESET") || + errorMessage.includes("Connection lost") || + errorMessage.includes("ETIMEDOUT") + ) { + retryCount++; + if (retryCount < maxRetries) { + console.log(`🔄 จะลองใหม่ในอีก 5 วินาที... (${retryCount}/${maxRetries})`); + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Reset database connection + try { + if (AppDataSource.isInitialized) { + await AppDataSource.destroy(); + } + await AppDataSource.initialize(); + } catch (reconnectErr) { + console.warn("⚠️ ไม่สามารถเชื่อมต่อฐานข้อมูลใหม่ได้:", reconnectErr); + } + continue; + } + } + + throw err; } + } - const salaryOrg = await this.salaryOrgRepository.find({ + throw new Error(`❌ Snapshot ล้มเหลวหลังจากลองแล้ว ${maxRetries} ครั้ง`); + } + + private async performSnapshotOperation( + snapshot: string, + salaryPeriodId: string, + request: RequestWithUser, + ) { + const salaryPeriod = await this.salaryPeriodRepository.findOne({ + where: { id: salaryPeriodId }, + }); + if (!salaryPeriod) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบรอบการสร้างเงินเดือน"); + } + + // Parallel loading of existing data for removal + console.time("⏱ Cleanup: Load existing data"); + const [salaryOrg, salaryOrgEmployee] = await Promise.all([ + this.salaryOrgRepository.find({ where: { salaryPeriodId: salaryPeriod.id, snapshot: snapshot }, - }); - const salaryProfile = await this.salaryProfileRepository.find({ - where: { salaryOrgId: In(salaryOrg.map((x) => x.id)) }, - }); - await this.salaryProfileRepository.remove(salaryProfile, { data: request }); - await this.salaryOrgRepository.remove(salaryOrg, { data: request }); - - const salaryOrgEmployee = await this.salaryOrgEmployeeRepository.find({ + }), + this.salaryOrgEmployeeRepository.find({ where: { salaryPeriodId: salaryPeriod.id, snapshot: snapshot }, - }); - const salaryProfileEmployee = await this.salaryProfileEmployeeRepository.find({ - where: { salaryOrgId: In(salaryOrgEmployee.map((x) => x.id)) }, - }); - await this.salaryProfileEmployeeRepository.remove(salaryProfileEmployee, { data: request }); - await this.salaryOrgEmployeeRepository.remove(salaryOrgEmployee, { data: request }); - let orgs = await new CallAPI().GetData(request, "/org/unauthorize/active/root/id"); + }), + ]); - //snap บางสำนัก - //.chin - // const targetRootIds = [ - // "d7e98989-b5ce-47d6-93c3-ab63ed486348", - // "e0545eca-5d0a-4a1c-8bbd-e3e25c2521db", - // "26989ffa-d5ab-4bbd-ac97-130646cd1da6", - // "6f9b30e1-757a-40d5-b053-61eb1b91c0f0", - // "eaf65f33-25e9-4956-9dba-5d909f5eb595", - // "a3efed2c-3f4b-476d-95e6-9f7e0585ae25", - // ]; - //.me - // const targetRootIds = [ - // "b89a4467-7ee3-4706-8db7-f366555f826c", - // "585648a9-e634-43fc-9360-5fd4189136ab", - // "d6e3daa0-284a-428f-aa43-0750fa74e974", - // "ed3ddcfb-f882-4499-817b-aff73e5be87c", - // "f8ce98ca-a691-4c89-abde-875f559afb3a", - // ]; + // Parallel loading of profiles for removal + const [salaryProfile, salaryProfileEmployee] = await Promise.all([ + salaryOrg.length > 0 + ? this.salaryProfileRepository.find({ + where: { salaryOrgId: In(salaryOrg.map((x) => x.id)) }, + }) + : Promise.resolve([]), + salaryOrgEmployee.length > 0 + ? this.salaryProfileEmployeeRepository.find({ + where: { salaryOrgId: In(salaryOrgEmployee.map((x) => x.id)) }, + }) + : Promise.resolve([]), + ]); + console.timeEnd("⏱ Cleanup: Load existing data"); - // orgs = orgs.filter((x: any) => targetRootIds.includes(x.rootId)); - let total = 1000; - // let _orgProfiles = await new CallAPI().PostData(request, "/org/unauthorize/salary/gen", { - let _orgProfiles = await new CallAPI().PostData(request, "/org/unauthorize/new-salary/gen", { + console.log( + `🧹 ลบข้อมูลเก่า: SalaryProfile=${salaryProfile.length}, SalaryProfileEmployee=${salaryProfileEmployee.length}, SalaryOrg=${salaryOrg.length}, SalaryOrgEmployee=${salaryOrgEmployee.length}`, + ); + + // Sequential removal to respect foreign key constraints + // First, remove all profiles (child records) + console.time("⏱ Cleanup: Remove child records"); + await Promise.all([ + salaryProfile.length > 0 + ? this.salaryProfileRepository.remove(salaryProfile, { data: request }) + : Promise.resolve(), + salaryProfileEmployee.length > 0 + ? this.salaryProfileEmployeeRepository.remove(salaryProfileEmployee, { data: request }) + : Promise.resolve(), + ]); + console.timeEnd("⏱ Cleanup: Remove child records"); + + // Then, remove the org records (parent records) + console.time("⏱ Cleanup: Remove parent records"); + await Promise.all([ + salaryOrg.length > 0 + ? this.salaryOrgRepository.remove(salaryOrg, { data: request }) + : Promise.resolve(), + salaryOrgEmployee.length > 0 + ? this.salaryOrgEmployeeRepository.remove(salaryOrgEmployee, { data: request }) + : Promise.resolve(), + ]); + console.timeEnd("⏱ Cleanup: Remove parent records"); + + //snap บางสำนัก + //.chin + // const targetRootIds = [ + // "d7e98989-b5ce-47d6-93c3-ab63ed486348", + // "e0545eca-5d0a-4a1c-8bbd-e3e25c2521db", + // "26989ffa-d5ab-4bbd-ac97-130646cd1da6", + // "6f9b30e1-757a-40d5-b053-61eb1b91c0f0", + // "eaf65f33-25e9-4956-9dba-5d909f5eb595", + // "a3efed2c-3f4b-476d-95e6-9f7e0585ae25", + // ]; + //.me + // const targetRootIds = [ + // "b89a4467-7ee3-4706-8db7-f366555f826c", + // "585648a9-e634-43fc-9360-5fd4189136ab", + // "d6e3daa0-284a-428f-aa43-0750fa74e974", + // "ed3ddcfb-f882-4499-817b-aff73e5be87c", + // "f8ce98ca-a691-4c89-abde-875f559afb3a", + // ]; + + // orgs = orgs.filter((x: any) => targetRootIds.includes(x.rootId)); + + // Parallel loading of initial data and API calls + console.time("⏱ API: Load initial data"); + const [orgs, revisionId, _orgProfiles, _orgProfileEmployees] = await Promise.all([ + new CallAPI().GetData(request, "/org/unauthorize/active/root/id"), + new CallAPI().GetData(request, "/org/unauthorize/revision/latest"), + new CallAPI().PostData(request, "/org/unauthorize/new-salary/gen", { page: 1, pageSize: 1000, keyword: "", year: salaryPeriod.year, period: salaryPeriod.period, - }); - let orgProfiles = _orgProfiles.data; - total = _orgProfiles.total; - console.log(`total: ${total}`); + }), + new CallAPI().PostData(request, "/org/unauthorize/new-salary/employee/gen", { + page: 1, + pageSize: 1000, + keyword: "", + year: salaryPeriod.year, + period: salaryPeriod.period, + }), + ]); + console.timeEnd("⏱ API: Load initial data"); - if (total > 1000) { - const page = Math.ceil(total / 1000); - for (let index = 2; index <= page; index++) { - await new CallAPI() - // .PostData(request, "/org/unauthorize/salary/gen", { - .PostData(request, "/org/unauthorize/new-salary/gen", { - page: index, - pageSize: 1000, - keyword: "", - year: salaryPeriod.year, - period: salaryPeriod.period, - }) - .then((x) => { - Array.prototype.push.apply(orgProfiles, x.data); - }); - } - } - total = 1000; - let orgProfileEmployees: any; - let _orgProfileEmployees = await new CallAPI().PostData( - request, - // "/org/unauthorize/salary/employee/gen", - "/org/unauthorize/new-salary/employee/gen", - { - page: 1, - pageSize: 1000, - keyword: "", - year: salaryPeriod.year, - period: salaryPeriod.period, - }, - ); - orgProfileEmployees = _orgProfileEmployees.data; - total = _orgProfileEmployees.total; - console.log(`totalEmp: ${total}`); - if (total > 1000) { - const page = Math.ceil(total / 1000); - for (let index = 2; index <= page; index++) { - await new CallAPI() - // .PostData(request, "/org/unauthorize/salary/employee/gen", { - .PostData(request, "/org/unauthorize/new-salary/employee/gen", { - page: index, - pageSize: 1000, - keyword: "", - year: salaryPeriod.year, - period: salaryPeriod.period, - }) - .then((x) => { - Array.prototype.push.apply(orgProfileEmployees, x.data); - }); - } - } + let total = 1000; + let orgProfiles = _orgProfiles.data; + total = _orgProfiles.total; + console.log(`total: ${total}`); - let revisionId = await new CallAPI().GetData(request, "/org/unauthorize/revision/latest"); - let _null: any = null; - // const beforeSalaryPeriod = structuredClone(salaryPeriod); - salaryPeriod.revisionId = revisionId; - salaryPeriod.lastUpdateUserId = request.user.sub; - salaryPeriod.lastUpdateFullName = request.user.name; - salaryPeriod.lastUpdatedAt = new Date(); - await this.salaryPeriodRepository.save(salaryPeriod, { data: request }); - // setLogDataDiff(request, { before: beforeSalaryPeriod, after: salaryPeriod }); - - await Promise.all( - orgs.map(async (root: any) => { - let salaryOrgNew = Object.assign(new SalaryOrg()); - delete salaryOrgNew.id; - // const beforeSalaryOrgNew = structuredClone(salaryOrgNew); - - salaryOrgNew.salaryPeriodId = salaryPeriod.id; - salaryOrgNew.status = "PENDING"; - salaryOrgNew.rootId = root.rootId; - salaryOrgNew.rootDnaId = root.rootDnaId ?? _null; - salaryOrgNew.root = root.root; - salaryOrgNew.revisionId = salaryPeriod.revisionId; - salaryOrgNew.snapshot = snapshot; - salaryOrgNew.createdUserId = request.user.sub; - salaryOrgNew.createdFullName = request.user.name; - salaryOrgNew.lastUpdateUserId = request.user.sub; - salaryOrgNew.lastUpdateFullName = request.user.name; - salaryOrgNew.createdAt = new Date(); - salaryOrgNew.lastUpdatedAt = new Date(); - salaryOrgNew.group = "GROUP1"; - await this.salaryOrgRepository.save(salaryOrgNew, { data: request }); - // setLogDataDiff(request, { before: beforeSalaryOrgNew, after: salaryOrgNew }); - console.log(`✅ [SNAP: ${snapshot}] บันทึก salaryOrgNew: ${salaryOrgNew.id}`); - if (salaryPeriod.period != "SPECIAL") { - delete salaryOrgNew.id; - salaryOrgNew.group = "GROUP2"; - await this.salaryOrgRepository.save(salaryOrgNew, { data: request }); - // setLogDataDiff(request, { before: beforeSalaryOrgNew, after: salaryOrgNew }); - } - }), - ); - - await Promise.all( - orgs.map(async (root: any) => { - let salaryOrgNew = Object.assign(new SalaryOrgEmployee()); - // const beforeSalaryOrgNew = structuredClone(salaryOrgNew); - - salaryOrgNew.salaryPeriodId = salaryPeriod.id; - salaryOrgNew.status = "PENDING"; - salaryOrgNew.rootId = root.rootId; - salaryOrgNew.rootDnaId = root.rootDnaId ?? _null; - salaryOrgNew.root = root.root; - salaryOrgNew.revisionId = salaryPeriod.revisionId; - salaryOrgNew.snapshot = snapshot; - salaryOrgNew.createdUserId = request.user.sub; - salaryOrgNew.createdFullName = request.user.name; - salaryOrgNew.lastUpdateUserId = request.user.sub; - salaryOrgNew.lastUpdateFullName = request.user.name; - salaryOrgNew.group = "GROUP1"; - salaryOrgNew.createdAt = new Date(); - salaryOrgNew.lastUpdatedAt = new Date(); - await this.salaryOrgEmployeeRepository.save(salaryOrgNew, { data: request }); - // setLogDataDiff(request, { before: beforeSalaryOrgNew, after: salaryOrgNew }); - console.log(`✅ [SNAP: ${snapshot}] บันทึก salaryOrgEmployeeNew: ${salaryOrgNew.id}`); - if (salaryPeriod.period != "SPECIAL") { - delete salaryOrgNew.id; - salaryOrgNew.group = "GROUP2"; - await this.salaryOrgEmployeeRepository.save(salaryOrgNew, { data: request }); - // setLogDataDiff(request, { before: beforeSalaryOrgNew, after: salaryOrgNew }); - } - }), - ); - if (salaryPeriod.period != "SPECIAL") { - //console.log(`step1`); - console.time("⏱ Step1: Load SalaryOrg (New)"); - const salaryOrgList = await this.salaryOrgRepository.find({ - where: { salaryPeriodId: salaryPeriod.id, snapshot }, - }); - const salaryOrgMap = new Map( - salaryOrgList.map((org) => [`${org.rootId}-${org.group}`, org]), + if (total > 1000) { + console.time("⏱ API: Load additional profile pages"); + const page = Math.ceil(total / 1000); + const promises = []; + for (let index = 2; index <= page; index++) { + promises.push( + new CallAPI().PostData(request, "/org/unauthorize/new-salary/gen", { + page: index, + pageSize: 1000, + keyword: "", + year: salaryPeriod.year, + period: salaryPeriod.period, + }), ); - console.timeEnd("⏱ Step1: Load SalaryOrg (New)"); + } + const results = await Promise.all(promises); + results.forEach((x) => { + Array.prototype.push.apply(orgProfiles, x.data); + }); + console.timeEnd("⏱ API: Load additional profile pages"); + } + total = 1000; + let orgProfileEmployees = _orgProfileEmployees.data; + total = _orgProfileEmployees.total; + console.log(`totalEmp: ${total}`); + if (total > 1000) { + console.time("⏱ API: Load additional employee pages"); + const page = Math.ceil(total / 1000); + const promises = []; + for (let index = 2; index <= page; index++) { + promises.push( + new CallAPI().PostData(request, "/org/unauthorize/new-salary/employee/gen", { + page: index, + pageSize: 1000, + keyword: "", + year: salaryPeriod.year, + period: salaryPeriod.period, + }), + ); + } + const results = await Promise.all(promises); + results.forEach((x) => { + Array.prototype.push.apply(orgProfileEmployees, x.data); + }); + console.timeEnd("⏱ API: Load additional employee pages"); + } - console.time("⏱ Step2: Load SalaryProfileOld (SNAP1)"); - let salaryOldMap = new Map(); - if (snapshot === "SNAP2") { - const salaryOldList = await this.salaryProfileRepository - .createQueryBuilder("profile") - .innerJoin("profile.salaryOrg", "org") - .where("org.salaryPeriodId = :periodId", { periodId: salaryPeriod.id }) - .andWhere("org.snapshot = :snapshot", { snapshot: "SNAP1" }) - .getMany(); + let _null: any = null; + // const beforeSalaryPeriod = structuredClone(salaryPeriod); + salaryPeriod.revisionId = revisionId; + salaryPeriod.lastUpdateUserId = request.user.sub; + salaryPeriod.lastUpdateFullName = request.user.name; + salaryPeriod.lastUpdatedAt = new Date(); + await this.salaryPeriodRepository.save(salaryPeriod, { data: request }); + // setLogDataDiff(request, { before: beforeSalaryPeriod, after: salaryPeriod }); - salaryOldMap = new Map(salaryOldList.map((item) => [item.citizenId, item])); + // Prepare SalaryOrg records for batch insert + const salaryOrgsToSave: SalaryOrg[] = []; + + orgs.forEach((root: any) => { + // GROUP1 + let salaryOrgNew = Object.assign(new SalaryOrg()); + delete salaryOrgNew.id; + salaryOrgNew.salaryPeriodId = salaryPeriod.id; + salaryOrgNew.status = "PENDING"; + salaryOrgNew.rootId = root.rootId; + salaryOrgNew.rootDnaId = root.rootDnaId ?? _null; + salaryOrgNew.root = root.root; + salaryOrgNew.revisionId = salaryPeriod.revisionId; + salaryOrgNew.snapshot = snapshot; + salaryOrgNew.createdUserId = request.user.sub; + salaryOrgNew.createdFullName = request.user.name; + salaryOrgNew.lastUpdateUserId = request.user.sub; + salaryOrgNew.lastUpdateFullName = request.user.name; + salaryOrgNew.createdAt = new Date(); + salaryOrgNew.lastUpdatedAt = new Date(); + salaryOrgNew.group = "GROUP1"; + salaryOrgsToSave.push(salaryOrgNew); + + // GROUP2 (if not SPECIAL) + if (salaryPeriod.period != "SPECIAL") { + let salaryOrgNew2 = Object.assign(new SalaryOrg(), salaryOrgNew); + delete salaryOrgNew2.id; + salaryOrgNew2.group = "GROUP2"; + salaryOrgsToSave.push(salaryOrgNew2); + } + }); + + // Batch insert SalaryOrg + console.time("⏱ Insert: SalaryOrg batch"); + if (salaryOrgsToSave.length > 0) { + await this.salaryOrgRepository.save(salaryOrgsToSave, { data: request }); + console.log(`✅ [SNAP: ${snapshot}] บันทึก ${salaryOrgsToSave.length} SalaryOrg สำเร็จ`); + } + console.timeEnd("⏱ Insert: SalaryOrg batch"); + + // Prepare SalaryOrgEmployee records for batch insert + const salaryOrgEmployeesToSave: SalaryOrgEmployee[] = []; + + orgs.forEach((root: any) => { + // GROUP1 + let salaryOrgNew = Object.assign(new SalaryOrgEmployee()); + salaryOrgNew.salaryPeriodId = salaryPeriod.id; + salaryOrgNew.status = "PENDING"; + salaryOrgNew.rootId = root.rootId; + salaryOrgNew.rootDnaId = root.rootDnaId ?? _null; + salaryOrgNew.root = root.root; + salaryOrgNew.revisionId = salaryPeriod.revisionId; + salaryOrgNew.snapshot = snapshot; + salaryOrgNew.createdUserId = request.user.sub; + salaryOrgNew.createdFullName = request.user.name; + salaryOrgNew.lastUpdateUserId = request.user.sub; + salaryOrgNew.lastUpdateFullName = request.user.name; + salaryOrgNew.group = "GROUP1"; + salaryOrgNew.createdAt = new Date(); + salaryOrgNew.lastUpdatedAt = new Date(); + salaryOrgEmployeesToSave.push(salaryOrgNew); + + // GROUP2 (if not SPECIAL) + if (salaryPeriod.period != "SPECIAL") { + let salaryOrgNew2 = Object.assign(new SalaryOrgEmployee(), salaryOrgNew); + delete salaryOrgNew2.id; + salaryOrgNew2.group = "GROUP2"; + salaryOrgEmployeesToSave.push(salaryOrgNew2); + } + }); + + // Batch insert SalaryOrgEmployee + console.time("⏱ Insert: SalaryOrgEmployee batch"); + if (salaryOrgEmployeesToSave.length > 0) { + await this.salaryOrgEmployeeRepository.save(salaryOrgEmployeesToSave, { data: request }); + console.log( + `✅ [SNAP: ${snapshot}] บันทึก ${salaryOrgEmployeesToSave.length} SalaryOrgEmployee สำเร็จ`, + ); + } + console.timeEnd("⏱ Insert: SalaryOrgEmployee batch"); + if (salaryPeriod.period != "SPECIAL") { + //console.log(`step1`); + console.time("⏱ Step1: Load SalaryOrg (New)"); + const salaryOrgList = await this.salaryOrgRepository.find({ + where: { salaryPeriodId: salaryPeriod.id, snapshot }, + }); + const salaryOrgMap = new Map(salaryOrgList.map((org) => [`${org.rootId}-${org.group}`, org])); + console.timeEnd("⏱ Step1: Load SalaryOrg (New)"); + + console.time("⏱ Step2: Load SalaryProfileOld (SNAP1)"); + let salaryOldMap = new Map(); + if (snapshot === "SNAP2") { + const salaryOldList = await this.salaryProfileRepository + .createQueryBuilder("profile") + .innerJoin("profile.salaryOrg", "org") + .where("org.salaryPeriodId = :periodId", { periodId: salaryPeriod.id }) + .andWhere("org.snapshot = :snapshot", { snapshot: "SNAP1" }) + .getMany(); + + salaryOldMap = new Map(salaryOldList.map((item) => [item.citizenId, item])); + } + console.timeEnd("⏱ Step2: Load SalaryProfileOld (SNAP1)"); + + console.time("⏱ Step3: Process Profiles"); + + const allProfilesToSave: SalaryProfile[] = []; + + for (const profile of orgProfiles) { + let group = "GROUP1"; + const { posType, posLevel } = profile; + + if ( + (posType === "ทั่วไป" && posLevel === "ทักษะพิเศษ") || + (posType === "วิชาการ" && ["เชี่ยวชาญ", "ทรงคุณวุฒิ"].includes(posLevel)) || + (posType === "อำนวยการ" && posLevel === "สูง") || + (posType === "บริหาร" && ["ต้น", "สูง"].includes(posLevel)) + ) { + group = "GROUP2"; } - console.timeEnd("⏱ Step2: Load SalaryProfileOld (SNAP1)"); - console.time("⏱ Step3: Process Profiles"); + const salaryOrgNew = salaryOrgMap.get(`${profile.rootId}-${group}`); + if (!salaryOrgNew) { + console.warn(`⚠️ [SNAP: ${snapshot}] ไม่พบ salaryOrg สำหรับ rootId: ${profile.rootId}`); + continue; + } - const allProfilesToSave: SalaryProfile[] = []; + const salaryProfileNew = Object.assign(new SalaryProfile(), profile); + delete salaryProfileNew.id; - for (const profile of orgProfiles) { - let group = "GROUP1"; - const { posType, posLevel } = profile; + salaryProfileNew.salaryOrgId = salaryOrgNew.id; + salaryProfileNew.revisionId = salaryPeriod.revisionId; + salaryProfileNew.createdUserId = request.user.sub; + salaryProfileNew.createdFullName = request.user.name; + salaryProfileNew.lastUpdateUserId = request.user.sub; + salaryProfileNew.lastUpdateFullName = request.user.name; + salaryProfileNew.createdAt = new Date(); + salaryProfileNew.lastUpdatedAt = new Date(); - if ( - (posType === "ทั่วไป" && posLevel === "ทักษะพิเศษ") || - (posType === "วิชาการ" && ["เชี่ยวชาญ", "ทรงคุณวุฒิ"].includes(posLevel)) || - (posType === "อำนวยการ" && posLevel === "สูง") || - (posType === "บริหาร" && ["ต้น", "สูง"].includes(posLevel)) - ) { - group = "GROUP2"; + if (snapshot === "SNAP2") { + const salaryOld = salaryOldMap.get(salaryProfileNew.citizenId); + salaryProfileNew.type = salaryOld?.type ?? "PENDING"; + salaryProfileNew.amount = salaryOld?.amount ?? 0; + salaryProfileNew.amountSpecial = salaryOld?.amountSpecial ?? 0; + salaryProfileNew.amountUse = salaryOld?.amountUse ?? 0; + salaryProfileNew.positionSalaryAmount = salaryOld?.positionSalaryAmount ?? 0; + salaryProfileNew.remark = salaryOld?.remark ?? null; + salaryProfileNew.isNext = salaryOld?.isNext ?? false; + salaryProfileNew.isSpecial = salaryOld?.isSpecial ?? false; + salaryProfileNew.isReserve = salaryOld?.isReserve ?? false; + salaryProfileNew.isRetired = salaryOld?.isRetired ?? false; + salaryProfileNew.isGood = salaryOld?.isGood ?? false; + } + + // const beforeSalaryProfileNew = structuredClone(salaryProfileNew); + // setLogDataDiff(request, { before: beforeSalaryProfileNew, after: salaryProfileNew }); + allProfilesToSave.push(salaryProfileNew); + } + + console.timeEnd("⏱ Step3: Process Profiles"); + + // Batch insert SalaryProfile with chunking for memory efficiency + console.time("⏱ Step4: Save All Profiles (Chunked)"); + const chunkSize = 500; // Reduced chunk size to prevent timeout + for (let i = 0; i < allProfilesToSave.length; i += chunkSize) { + const chunk = allProfilesToSave.slice(i, i + chunkSize); + try { + await this.salaryProfileRepository + .createQueryBuilder() + .insert() + .into(SalaryProfile) + .values(chunk) + .orIgnore() + .execute(); + + // Progress logging every 10 chunks + if ((i / chunkSize) % 10 === 0) { + console.log(`📝 บันทึก SalaryProfile: ${i + chunk.length}/${allProfilesToSave.length}`); } + } catch (chunkErr) { + console.error(`❌ Error saving SalaryProfile chunk ${i}-${i + chunk.length}:`, chunkErr); + throw chunkErr; + } + } + console.timeEnd("⏱ Step4: Save All Profiles (Chunked)"); + console.log( + `✅ บันทึก ${allProfilesToSave.length} SalaryProfile สำเร็จ (แบ่งเป็น chunks ของ ${chunkSize})`, + ); - const salaryOrgNew = salaryOrgMap.get(`${profile.rootId}-${group}`); - if (!salaryOrgNew) { - console.warn(`⚠️ [SNAP: ${snapshot}] ไม่พบ salaryOrg สำหรับ rootId: ${profile.rootId}`); - continue; - } + //**********************/ + console.log("orgProfileEmployees length:", orgProfileEmployees.length); + const profilesEmpToSave: SalaryProfileEmployee[] = []; + console.time("⏱ SalaryProfileEmployee - Total Time"); + console.time("⏱ Step5: Process ProfileEmps"); - const salaryProfileNew = Object.assign(new SalaryProfile(), profile); + // Pre-load salaryOrgEmployee เพื่อลด query + const salaryOrgEmployeeMap = new Map(); + const salaryOrgEmployeeList = await this.salaryOrgEmployeeRepository.find({ + where: { + salaryPeriodId: salaryPeriod.id, + snapshot: snapshot, + group: "GROUP1", + }, + }); + salaryOrgEmployeeList.forEach((org) => { + salaryOrgEmployeeMap.set(org.rootId, org); + }); + + // Pre-load old data for SNAP2 + let salaryEmpOldMap = new Map(); + if (snapshot == "SNAP2") { + const salaryOrgOld = await this.salaryOrgEmployeeRepository.find({ + where: { salaryPeriodId: salaryPeriod.id, snapshot: "SNAP1" }, + }); + if (salaryOrgOld.length > 0) { + const salaryOldList = await this.salaryProfileEmployeeRepository.find({ + where: { + salaryOrgId: In(salaryOrgOld.map((x) => x.id)), + }, + }); + salaryOldList.forEach((item) => { + salaryEmpOldMap.set(item.citizenId, item); + }); + } + } + + for (const profile of orgProfileEmployees) { + const salaryOrgNew = salaryOrgEmployeeMap.get(profile.rootId); + if (salaryOrgNew != null) { + let salaryProfileNew = Object.assign(new SalaryProfileEmployee(), profile); delete salaryProfileNew.id; salaryProfileNew.salaryOrgId = salaryOrgNew.id; @@ -2780,13 +3005,17 @@ export class SalaryPeriodController extends Controller { salaryProfileNew.createdAt = new Date(); salaryProfileNew.lastUpdatedAt = new Date(); - if (snapshot === "SNAP2") { - const salaryOld = salaryOldMap.get(salaryProfileNew.citizenId); - salaryProfileNew.type = salaryOld?.type ?? "PENDING"; + if (snapshot == "SNAP2") { + const salaryOld = salaryEmpOldMap.get(salaryProfileNew.citizenId); + salaryProfileNew.type = salaryOld?.type ?? 0; + salaryProfileNew.salaryLevelNew = salaryOld?.salaryLevelNew ?? null; + salaryProfileNew.groupNew = salaryOld?.groupNew ?? null; salaryProfileNew.amount = salaryOld?.amount ?? 0; salaryProfileNew.amountSpecial = salaryOld?.amountSpecial ?? 0; salaryProfileNew.amountUse = salaryOld?.amountUse ?? 0; salaryProfileNew.positionSalaryAmount = salaryOld?.positionSalaryAmount ?? 0; + salaryProfileNew.positionSalaryDayAmount = salaryOld?.positionSalaryDayAmount ?? 0; + salaryProfileNew.positionSalaryAmountPer = salaryOld?.positionSalaryAmountPer ?? 0; salaryProfileNew.remark = salaryOld?.remark ?? null; salaryProfileNew.isNext = salaryOld?.isNext ?? false; salaryProfileNew.isSpecial = salaryOld?.isSpecial ?? false; @@ -2794,397 +3023,364 @@ export class SalaryPeriodController extends Controller { salaryProfileNew.isRetired = salaryOld?.isRetired ?? false; salaryProfileNew.isGood = salaryOld?.isGood ?? false; } - - // const beforeSalaryProfileNew = structuredClone(salaryProfileNew); - // setLogDataDiff(request, { before: beforeSalaryProfileNew, after: salaryProfileNew }); - allProfilesToSave.push(salaryProfileNew); + profilesEmpToSave.push(salaryProfileNew); + } else { + console.warn( + `⚠️ [SNAP: ${snapshot}] ไม่พบ salaryOrgEmployee สำหรับ rootId: ${profile.rootId}`, + ); } + } + console.timeEnd("⏱ Step5: Process ProfileEmps"); - console.timeEnd("⏱ Step3: Process Profiles"); - - console.time("⏱ Step4: Save All Profiles"); - // await this.salaryProfileRepository.save(allProfilesToSave, { chunk: 500, data: request }); - - await this.salaryProfileRepository - .createQueryBuilder() - .insert() - .into(SalaryProfile) - .values(allProfilesToSave) - .orIgnore() - .execute(); - console.timeEnd("⏱ Step4: Save All Profiles"); - - console.timeEnd("⏱ TOTAL SalaryProfile Process"); - - //**********************/ - console.log("mlength", orgProfileEmployees.length); - const profilesEmpToSave: SalaryProfileEmployee[] = []; - console.time("⏱ SalaryProfileEmployee - Total Time"); - console.time("⏱ Step5: Process ProfileEmps"); - for (const profile of orgProfileEmployees) { - const salaryOrgNew = await this.salaryOrgEmployeeRepository.findOne({ - where: { - salaryPeriodId: salaryPeriod.id, - rootId: profile.rootId, - snapshot: snapshot, - group: "GROUP1", - }, - }); - //console.log(`step7`); - if (salaryOrgNew != null) { - //console.log(`step8`); - let salaryProfileNew = Object.assign(new SalaryProfileEmployee(), profile); - delete salaryProfileNew.id; - // const beforeSalaryProfileNew = structuredClone(salaryProfileNew); - - salaryProfileNew.salaryOrgId = salaryOrgNew.id; - salaryProfileNew.revisionId = salaryPeriod.revisionId; - salaryProfileNew.createdUserId = request.user.sub; - salaryProfileNew.createdFullName = request.user.name; - salaryProfileNew.lastUpdateUserId = request.user.sub; - salaryProfileNew.lastUpdateFullName = request.user.name; - salaryProfileNew.createdAt = new Date(); - salaryProfileNew.lastUpdatedAt = new Date(); - - if (snapshot == "SNAP2") { - //console.log(`step9`); - const salaryOrgOld = await this.salaryOrgEmployeeRepository.find({ - where: { salaryPeriodId: salaryPeriod.id, snapshot: "SNAP1" }, - }); - const salaryOld = await this.salaryProfileEmployeeRepository.findOne({ - where: { - citizenId: salaryProfileNew.citizenId, - salaryOrgId: In(salaryOrgOld.map((x) => x.id)), - }, - }); - salaryProfileNew.type = salaryOld == null ? 0 : salaryOld.type; - // salaryProfileNew.salaryLevel = salaryOld && salaryOld.salaryLevelNew ? salaryOld.salaryLevelNew : salaryOld?.salaryLevel; - // salaryProfileNew.group = salaryOld && salaryOld.groupNew ? salaryOld.groupNew : salaryOld?.group; - salaryProfileNew.salaryLevelNew = - salaryOld && salaryOld.salaryLevelNew ? salaryOld.salaryLevelNew : null; - salaryProfileNew.groupNew = - salaryOld && salaryOld.groupNew ? salaryOld.groupNew : null; - salaryProfileNew.amount = salaryOld == null ? 0 : salaryOld.amount; - salaryProfileNew.amountSpecial = salaryOld == null ? 0 : salaryOld.amountSpecial; - salaryProfileNew.amountUse = salaryOld == null ? 0 : salaryOld.amountUse; - salaryProfileNew.positionSalaryAmount = - salaryOld == null ? 0 : salaryOld.positionSalaryAmount; - salaryProfileNew.positionSalaryDayAmount = - salaryOld == null ? 0 : salaryOld.positionSalaryDayAmount; - salaryProfileNew.positionSalaryAmountPer = - salaryOld == null ? 0 : salaryOld.positionSalaryAmountPer; - salaryProfileNew.remark = salaryOld == null ? null : salaryOld.remark; - salaryProfileNew.isNext = salaryOld == null ? false : salaryOld.isNext; - salaryProfileNew.isSpecial = salaryOld == null ? false : salaryOld.isSpecial; - salaryProfileNew.isReserve = salaryOld == null ? false : salaryOld.isReserve; - salaryProfileNew.isRetired = salaryOld == null ? false : salaryOld.isRetired; - salaryProfileNew.isGood = salaryOld == null ? false : salaryOld.isGood; - } - profilesEmpToSave.push(salaryProfileNew); + // Batch insert SalaryProfileEmployee with chunking + console.time("⏱ Step6: Save ProfileEmployees (Chunked)"); + if (profilesEmpToSave.length > 0) { + const chunkSize = 500; // Reduced chunk size for better stability + for (let i = 0; i < profilesEmpToSave.length; i += chunkSize) { + const chunk = profilesEmpToSave.slice(i, i + chunkSize); + try { await this.salaryProfileEmployeeRepository .createQueryBuilder() .insert() .into(SalaryProfileEmployee) - .values(profilesEmpToSave) + .values(chunk) .orIgnore() .execute(); - //console.log(`step10`); - // console.log( - // `✅ [SNAP: ${snapshot}] Push SalaryProfileEmployee: ${salaryProfileNew.citizenId} (${salaryProfileNew.fullName ?? "-"})`, - // ); - // await this.salaryProfileEmployeeRepository.save(salaryProfileNew, { data: request }); - // await this.salaryProfileEmployeeRepository.save(profilesEmpToSave, { chunk: 100, data: request }); - // setLogDataDiff(request, { before: beforeSalaryProfileNew, after: salaryProfileNew }); - } else { - console.warn( - `⚠️ [SNAP: ${snapshot}] ไม่พบ salaryOrgEmployee สำหรับ rootId: ${profile.rootId}`, + + // Progress logging every 10 chunks + if ((i / chunkSize) % 10 === 0) { + console.log( + `📝 บันทึก SalaryProfileEmployee: ${i + chunk.length}/${profilesEmpToSave.length}`, + ); + } + } catch (chunkErr) { + console.error( + `❌ Error saving SalaryProfileEmployee chunk ${i}-${i + chunk.length}:`, + chunkErr, ); + throw chunkErr; } } - console.timeEnd("⏱ SalaryProfileEmployee - Total Time"); - } - - const salaryOrgNew = await this.salaryOrgRepository.find({ - where: { salaryPeriodId: salaryPeriod.id, snapshot: snapshot }, - relations: ["salaryProfiles"], - }); - const salaryOrgEmployeeNew = await this.salaryOrgEmployeeRepository.find({ - where: { salaryPeriodId: salaryPeriod.id, snapshot: snapshot }, - relations: ["salaryProfiles"], - }); - if (salaryPeriod.period == "OCT") { - const salaryPeriodAPROld = await this.salaryPeriodRepository.findOne({ - where: { - year: salaryPeriod.year, - period: "APR", - }, - }); - await Promise.all( - salaryOrgNew.map(async (_salaryOrg: SalaryOrg) => { - let totalAmount = 0; - if (salaryPeriodAPROld != null) { - const salaryOrgSnap2Old: any = await this.salaryOrgRepository.findOne({ - where: { - salaryPeriodId: salaryPeriodAPROld.id, - rootDnaId: _salaryOrg.rootDnaId, - group: _salaryOrg.group, - snapshot: "SNAP2", - }, - relations: ["salaryProfiles"], - }); - totalAmount = - salaryOrgSnap2Old == null - ? 0 - : Extension.sumObjectValues(salaryOrgSnap2Old.salaryProfiles, "amountUse"); - } - - const before_salaryOrg = structuredClone(_salaryOrg); - if (snapshot == "SNAP2") { - const salaryOrgSnap1 = await this.salaryOrgRepository.findOne({ - where: { - salaryPeriodId: salaryPeriod.id, - rootDnaId: _salaryOrg.rootDnaId, - group: _salaryOrg.group, - snapshot: "SNAP1", - }, - }); - if (salaryOrgSnap1 == null) { - const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); - _salaryOrg.currentAmount = totalProfile; - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.sixPercentAmount = totalProfile * 0.06; - _salaryOrg.spentAmount = totalAmount; - _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; - - //เพิ่มคำนวน 15% - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor( - (_salaryOrg.salaryProfiles.length * 15) / 100, - ); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - } else { - _salaryOrg.currentAmount = salaryOrgSnap1.currentAmount; - _salaryOrg.total = salaryOrgSnap1.total; - _salaryOrg.sixPercentAmount = salaryOrgSnap1.sixPercentAmount; - _salaryOrg.spentAmount = salaryOrgSnap1.spentAmount; - _salaryOrg.useAmount = salaryOrgSnap1.useAmount; - _salaryOrg.remainingAmount = salaryOrgSnap1.remainingAmount; - - //เพิ่มคำนวน 15% - _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; - _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; - _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; - _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; - } - } else { - const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); - - _salaryOrg.currentAmount = totalProfile; - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.sixPercentAmount = totalProfile * 0.06; - _salaryOrg.spentAmount = totalAmount; - _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; - - //เพิ่มคำนวน 15% - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - } - - _salaryOrg.createdUserId = request.user.sub; - _salaryOrg.createdFullName = request.user.name; - _salaryOrg.lastUpdateUserId = request.user.sub; - _salaryOrg.lastUpdateFullName = request.user.name; - _salaryOrg.createdAt = new Date(); - _salaryOrg.lastUpdatedAt = new Date(); - await this.salaryOrgRepository.save(_salaryOrg, { data: request }); - // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); - }), - ); - await Promise.all( - salaryOrgEmployeeNew.map(async (_salaryOrg: SalaryOrgEmployee) => { - let totalAmount = 0; - const before_salaryOrg = structuredClone(_salaryOrg); - - if (salaryPeriodAPROld != null) { - const salaryOrgSnap2Old: any = await this.salaryOrgEmployeeRepository.findOne({ - where: { - salaryPeriodId: salaryPeriodAPROld.id, - rootDnaId: _salaryOrg.rootDnaId, - group: _salaryOrg.group, - snapshot: "SNAP2", - }, - relations: ["salaryProfiles"], - }); - totalAmount = - salaryOrgSnap2Old == null - ? 0 - : Extension.sumObjectValues(salaryOrgSnap2Old.salaryProfiles, "amountUse"); - } - - if (snapshot == "SNAP2") { - const salaryOrgSnap1 = await this.salaryOrgEmployeeRepository.findOne({ - where: { - salaryPeriodId: salaryPeriod.id, - rootDnaId: _salaryOrg.rootDnaId, - group: _salaryOrg.group, - snapshot: "SNAP1", - }, - }); - if (salaryOrgSnap1 == null) { - const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); - _salaryOrg.currentAmount = totalProfile; - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.sixPercentAmount = totalProfile * 0.06; - _salaryOrg.spentAmount = totalAmount; - _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; - - //เพิ่มคำนวน 15% - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor( - (_salaryOrg.salaryProfiles.length * 15) / 100, - ); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - } else { - _salaryOrg.currentAmount = salaryOrgSnap1.currentAmount; - _salaryOrg.total = salaryOrgSnap1.total; - _salaryOrg.sixPercentAmount = salaryOrgSnap1.sixPercentAmount; - _salaryOrg.spentAmount = salaryOrgSnap1.spentAmount; - _salaryOrg.useAmount = salaryOrgSnap1.useAmount; - _salaryOrg.remainingAmount = salaryOrgSnap1.remainingAmount; - - //เพิ่มคำนวน 15% - _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; - _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; - _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; - _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; - } - } else { - const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); - _salaryOrg.currentAmount = totalProfile; - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.sixPercentAmount = totalProfile * 0.06; - _salaryOrg.spentAmount = totalAmount; - _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; - - //เพิ่มคำนวน 15% - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - } - - _salaryOrg.createdUserId = request.user.sub; - _salaryOrg.createdFullName = request.user.name; - _salaryOrg.lastUpdateUserId = request.user.sub; - _salaryOrg.lastUpdateFullName = request.user.name; - _salaryOrg.createdAt = new Date(); - _salaryOrg.lastUpdatedAt = new Date(); - await this.salaryOrgEmployeeRepository.save(_salaryOrg, { data: request }); - // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); - }), - ); - } else if (salaryPeriod.period == "APR") { - await Promise.all( - salaryOrgNew.map(async (_salaryOrg: SalaryOrg) => { - // const before_salaryOrg = structuredClone(_salaryOrg); - - if (snapshot == "SNAP2") { - const salaryOrgSnap1 = await this.salaryOrgRepository.findOne({ - where: { - salaryPeriodId: salaryPeriod.id, - rootDnaId: _salaryOrg.rootDnaId, - group: _salaryOrg.group, - snapshot: "SNAP1", - }, - }); - if (salaryOrgSnap1 == null) { - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor( - (_salaryOrg.salaryProfiles.length * 15) / 100, - ); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - } else { - _salaryOrg.total = salaryOrgSnap1.total; - _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; - _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; - _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; - _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; - } - } else { - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - } - - _salaryOrg.createdUserId = request.user.sub; - _salaryOrg.createdFullName = request.user.name; - _salaryOrg.lastUpdateUserId = request.user.sub; - _salaryOrg.lastUpdateFullName = request.user.name; - _salaryOrg.createdAt = new Date(); - _salaryOrg.lastUpdatedAt = new Date(); - await this.salaryOrgRepository.save(_salaryOrg, { data: request }); - // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); - }), - ); - await Promise.all( - salaryOrgEmployeeNew.map(async (_salaryOrg: SalaryOrgEmployee) => { - // const before_salaryOrg = structuredClone(_salaryOrg); - - if (snapshot == "SNAP2") { - const salaryOrgSnap1 = await this.salaryOrgEmployeeRepository.findOne({ - where: { - salaryPeriodId: salaryPeriod.id, - rootDnaId: _salaryOrg.rootDnaId, - group: _salaryOrg.group, - snapshot: "SNAP1", - }, - }); - if (salaryOrgSnap1 == null) { - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor( - (_salaryOrg.salaryProfiles.length * 15) / 100, - ); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - // console.log("fifteenAPR: case 1"); - } else { - _salaryOrg.total = salaryOrgSnap1.total; - _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; - _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; - _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; - _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; - // console.log("fifteenAPR: case 2"); - } - } else { - _salaryOrg.total = _salaryOrg.salaryProfiles.length; - _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; - _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); - // console.log("fifteenAPR: case 3"); - } - - _salaryOrg.createdUserId = request.user.sub; - _salaryOrg.createdFullName = request.user.name; - _salaryOrg.lastUpdateUserId = request.user.sub; - _salaryOrg.lastUpdateFullName = request.user.name; - _salaryOrg.createdAt = new Date(); - _salaryOrg.lastUpdatedAt = new Date(); - await this.salaryOrgEmployeeRepository.save(_salaryOrg, { data: request }); - // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); - }), + console.log( + `✅ บันทึก ${profilesEmpToSave.length} SalaryProfileEmployee สำเร็จ (แบ่งเป็น chunks ของ ${chunkSize})`, ); } - console.log(`✅✅✅ [SNAPSHOT:${snapshot} เสร็จสิ้น]) ✅✅✅`); - return new HttpSuccess(); - } catch (err) { - console.error(`❌ error processing employee:`, err); + console.timeEnd("⏱ Step6: Save ProfileEmployees (Chunked)"); + console.timeEnd("⏱ SalaryProfileEmployee - Total Time"); + + // Clear large arrays to free memory progressively + console.time("⏱ Memory: Cleanup large arrays"); + allProfilesToSave.length = 0; + profilesEmpToSave.length = 0; + orgProfiles.length = 0; + orgProfileEmployees.length = 0; + + // Clear Maps + salaryOrgMap.clear(); + salaryOldMap.clear(); + salaryOrgEmployeeMap.clear(); + salaryEmpOldMap.clear(); + + // Force garbage collection if available (Node.js with --expose-gc) + if (global.gc) { + global.gc(); + console.log("🧹 Garbage collection executed"); + } + console.timeEnd("⏱ Memory: Cleanup large arrays"); } + console.time("⏱ Final: Load Updated SalaryOrg"); + const salaryOrgNew = await this.salaryOrgRepository.find({ + where: { salaryPeriodId: salaryPeriod.id, snapshot: snapshot }, + relations: ["salaryProfiles"], + }); + const salaryOrgEmployeeNew = await this.salaryOrgEmployeeRepository.find({ + where: { salaryPeriodId: salaryPeriod.id, snapshot: snapshot }, + relations: ["salaryProfiles"], + }); + console.timeEnd("⏱ Final: Load Updated SalaryOrg"); + if (salaryPeriod.period == "OCT") { + console.time("⏱ Step7: Process OCT SalaryOrg"); + const salaryPeriodAPROld = await this.salaryPeriodRepository.findOne({ + where: { + year: salaryPeriod.year, + period: "APR", + }, + }); + + // Pre-load all old salary data for OCT optimization + let aprSalaryOrgMap = new Map(); + if (salaryPeriodAPROld != null) { + const aprSalaryOrgs = await this.salaryOrgRepository.find({ + where: { + salaryPeriodId: salaryPeriodAPROld.id, + snapshot: "SNAP2", + }, + relations: ["salaryProfiles"], + }); + aprSalaryOrgs.forEach((org) => { + const key = `${org.rootDnaId}-${org.group}`; + const totalAmount = Extension.sumObjectValues(org.salaryProfiles, "amountUse"); + aprSalaryOrgMap.set(key, totalAmount); + }); + } + + await Promise.all( + salaryOrgNew.map(async (_salaryOrg: SalaryOrg) => { + const totalAmount = + aprSalaryOrgMap.get(`${_salaryOrg.rootDnaId}-${_salaryOrg.group}`) || 0; + + // const before_salaryOrg = structuredClone(_salaryOrg); + if (snapshot == "SNAP2") { + const salaryOrgSnap1 = await this.salaryOrgRepository.findOne({ + where: { + salaryPeriodId: salaryPeriod.id, + rootDnaId: _salaryOrg.rootDnaId, + group: _salaryOrg.group, + snapshot: "SNAP1", + }, + }); + if (salaryOrgSnap1 == null) { + const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); + _salaryOrg.currentAmount = totalProfile; + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.sixPercentAmount = totalProfile * 0.06; + _salaryOrg.spentAmount = totalAmount; + _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; + + //เพิ่มคำนวน 15% + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + } else { + _salaryOrg.currentAmount = salaryOrgSnap1.currentAmount; + _salaryOrg.total = salaryOrgSnap1.total; + _salaryOrg.sixPercentAmount = salaryOrgSnap1.sixPercentAmount; + _salaryOrg.spentAmount = salaryOrgSnap1.spentAmount; + _salaryOrg.useAmount = salaryOrgSnap1.useAmount; + _salaryOrg.remainingAmount = salaryOrgSnap1.remainingAmount; + + //เพิ่มคำนวน 15% + _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; + _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; + _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; + _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; + } + } else { + const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); + + _salaryOrg.currentAmount = totalProfile; + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.sixPercentAmount = totalProfile * 0.06; + _salaryOrg.spentAmount = totalAmount; + _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; + + //เพิ่มคำนวน 15% + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + } + + _salaryOrg.createdUserId = request.user.sub; + _salaryOrg.createdFullName = request.user.name; + _salaryOrg.lastUpdateUserId = request.user.sub; + _salaryOrg.lastUpdateFullName = request.user.name; + _salaryOrg.createdAt = new Date(); + _salaryOrg.lastUpdatedAt = new Date(); + await this.salaryOrgRepository.save(_salaryOrg, { data: request }); + // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); + }), + ); + console.timeEnd("⏱ Step7: Process OCT SalaryOrg"); + + console.time("⏱ Step8: Process OCT SalaryOrgEmployee"); + // Pre-load APR employee salary data + let aprSalaryOrgEmployeeMap = new Map(); + if (salaryPeriodAPROld != null) { + const aprSalaryOrgEmployees = await this.salaryOrgEmployeeRepository.find({ + where: { + salaryPeriodId: salaryPeriodAPROld.id, + snapshot: "SNAP2", + }, + relations: ["salaryProfiles"], + }); + aprSalaryOrgEmployees.forEach((org) => { + const key = `${org.rootDnaId}-${org.group}`; + const totalAmount = Extension.sumObjectValues(org.salaryProfiles, "amountUse"); + aprSalaryOrgEmployeeMap.set(key, totalAmount); + }); + } + + await Promise.all( + salaryOrgEmployeeNew.map(async (_salaryOrg: SalaryOrgEmployee) => { + const totalAmount = + aprSalaryOrgEmployeeMap.get(`${_salaryOrg.rootDnaId}-${_salaryOrg.group}`) || 0; + // const before_salaryOrg = structuredClone(_salaryOrg); + + if (snapshot == "SNAP2") { + const salaryOrgSnap1 = await this.salaryOrgEmployeeRepository.findOne({ + where: { + salaryPeriodId: salaryPeriod.id, + rootDnaId: _salaryOrg.rootDnaId, + group: _salaryOrg.group, + snapshot: "SNAP1", + }, + }); + if (salaryOrgSnap1 == null) { + const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); + _salaryOrg.currentAmount = totalProfile; + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.sixPercentAmount = totalProfile * 0.06; + _salaryOrg.spentAmount = totalAmount; + _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; + + //เพิ่มคำนวน 15% + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + } else { + _salaryOrg.currentAmount = salaryOrgSnap1.currentAmount; + _salaryOrg.total = salaryOrgSnap1.total; + _salaryOrg.sixPercentAmount = salaryOrgSnap1.sixPercentAmount; + _salaryOrg.spentAmount = salaryOrgSnap1.spentAmount; + _salaryOrg.useAmount = salaryOrgSnap1.useAmount; + _salaryOrg.remainingAmount = salaryOrgSnap1.remainingAmount; + + //เพิ่มคำนวน 15% + _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; + _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; + _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; + _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; + } + } else { + const totalProfile = Extension.sumObjectValues(_salaryOrg.salaryProfiles, "amount"); + _salaryOrg.currentAmount = totalProfile; + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.sixPercentAmount = totalProfile * 0.06; + _salaryOrg.spentAmount = totalAmount; + _salaryOrg.remainingAmount = totalProfile * 0.06 - totalAmount; + + //เพิ่มคำนวน 15% + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + } + + _salaryOrg.createdUserId = request.user.sub; + _salaryOrg.createdFullName = request.user.name; + _salaryOrg.lastUpdateUserId = request.user.sub; + _salaryOrg.lastUpdateFullName = request.user.name; + _salaryOrg.createdAt = new Date(); + _salaryOrg.lastUpdatedAt = new Date(); + await this.salaryOrgEmployeeRepository.save(_salaryOrg, { data: request }); + // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); + }), + ); + console.timeEnd("⏱ Step8: Process OCT SalaryOrgEmployee"); + } else if (salaryPeriod.period == "APR") { + console.time("⏱ Step7: Process APR SalaryOrg"); + await Promise.all( + salaryOrgNew.map(async (_salaryOrg: SalaryOrg) => { + // const before_salaryOrg = structuredClone(_salaryOrg); + + if (snapshot == "SNAP2") { + const salaryOrgSnap1 = await this.salaryOrgRepository.findOne({ + where: { + salaryPeriodId: salaryPeriod.id, + rootDnaId: _salaryOrg.rootDnaId, + group: _salaryOrg.group, + snapshot: "SNAP1", + }, + }); + if (salaryOrgSnap1 == null) { + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + } else { + _salaryOrg.total = salaryOrgSnap1.total; + _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; + _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; + _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; + _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; + } + } else { + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + } + + _salaryOrg.createdUserId = request.user.sub; + _salaryOrg.createdFullName = request.user.name; + _salaryOrg.lastUpdateUserId = request.user.sub; + _salaryOrg.lastUpdateFullName = request.user.name; + _salaryOrg.createdAt = new Date(); + _salaryOrg.lastUpdatedAt = new Date(); + await this.salaryOrgRepository.save(_salaryOrg, { data: request }); + // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); + }), + ); + console.timeEnd("⏱ Step7: Process APR SalaryOrg"); + + console.time("⏱ Step8: Process APR SalaryOrgEmployee"); + await Promise.all( + salaryOrgEmployeeNew.map(async (_salaryOrg: SalaryOrgEmployee) => { + // const before_salaryOrg = structuredClone(_salaryOrg); + + if (snapshot == "SNAP2") { + const salaryOrgSnap1 = await this.salaryOrgEmployeeRepository.findOne({ + where: { + salaryPeriodId: salaryPeriod.id, + rootDnaId: _salaryOrg.rootDnaId, + group: _salaryOrg.group, + snapshot: "SNAP1", + }, + }); + if (salaryOrgSnap1 == null) { + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + // console.log("fifteenAPR: case 1"); + } else { + _salaryOrg.total = salaryOrgSnap1.total; + _salaryOrg.fifteenPercent = salaryOrgSnap1.fifteenPercent; + _salaryOrg.fifteenPoint = salaryOrgSnap1.fifteenPoint; + _salaryOrg.quantityUsed = salaryOrgSnap1.quantityUsed; + _salaryOrg.remainQuota = salaryOrgSnap1.remainQuota; + // console.log("fifteenAPR: case 2"); + } + } else { + _salaryOrg.total = _salaryOrg.salaryProfiles.length; + _salaryOrg.fifteenPercent = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + _salaryOrg.fifteenPoint = (_salaryOrg.salaryProfiles.length * 15) % 100; + _salaryOrg.remainQuota = Math.floor((_salaryOrg.salaryProfiles.length * 15) / 100); + // console.log("fifteenAPR: case 3"); + } + + _salaryOrg.createdUserId = request.user.sub; + _salaryOrg.createdFullName = request.user.name; + _salaryOrg.lastUpdateUserId = request.user.sub; + _salaryOrg.lastUpdateFullName = request.user.name; + _salaryOrg.createdAt = new Date(); + _salaryOrg.lastUpdatedAt = new Date(); + await this.salaryOrgEmployeeRepository.save(_salaryOrg, { data: request }); + // setLogDataDiff(request, { before: before_salaryOrg, after: _salaryOrg }); + }), + ); + console.timeEnd("⏱ Step8: Process APR SalaryOrgEmployee"); + } + + console.timeEnd("⏱ TOTAL SNAPSHOT PROCESSING TIME"); + console.log(`✅✅✅ [SNAPSHOT:${snapshot} เสร็จสิ้น - Period: ${salaryPeriod.period}]) ✅✅✅`); + console.log( + `📊 สรุปข้อมูล: SalaryOrg=${salaryOrgNew.length}, SalaryOrgEmployee=${salaryOrgEmployeeNew.length}`, + ); + return new HttpSuccess(); } /**