From b47c37e325bac9350a3c7b17aceed4648621b373 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 19 Jan 2026 13:21:46 +0700 Subject: [PATCH 1/3] test script change-multi --- src/controllers/SalaryPeriodController.ts | 1026 +++++++++++++++------ 1 file changed, 720 insertions(+), 306 deletions(-) diff --git a/src/controllers/SalaryPeriodController.ts b/src/controllers/SalaryPeriodController.ts index cf8d1d8..e213542 100644 --- a/src/controllers/SalaryPeriodController.ts +++ b/src/controllers/SalaryPeriodController.ts @@ -72,68 +72,68 @@ export class SalaryPeriodController extends Controller { const data = { group1id: salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.id, + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.id, group1IsClose: salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.isClose, + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.isClose, group2id: salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.id, + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.id, group2IsClose: salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgs && - salaryPeriod.salaryOrgs.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.isClose, + salaryPeriod.salaryOrgs.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.isClose, effectiveDate: salaryPeriod.effectiveDate, period: salaryPeriod.period, }; @@ -192,8 +192,8 @@ export class SalaryPeriodController extends Controller { _salaryOrgGROUP2 == null ? 0 : _salaryOrgGROUP2.salaryProfiles.reduce((accumulator, object) => { - return accumulator + object.amountSpecial; - }, 0); + return accumulator + object.amountSpecial; + }, 0); const data = { org: item.root, total: item.total + (_salaryOrgGROUP2 == null ? 0 : _salaryOrgGROUP2.total), @@ -583,9 +583,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -598,9 +598,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFull == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFull == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFull - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -613,9 +613,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFullHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFullHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFullHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -1051,9 +1051,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -1066,9 +1066,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFull == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFull == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFull - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -1081,9 +1081,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFullHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFullHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFullHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -1209,8 +1209,8 @@ export class SalaryPeriodController extends Controller { * @param {string} id profile Id * @param {string} type ประเภทการเลื่อน NONE->ไม่ได้เลื่อน HAFT->ครึ่งขั้น FULL->1ขั้น FULLHAFT->1.5ขั้น */ - @Post("change/type-multi") - async changeTypeMulti( + @Post("oldchange/type-multi") + async oldchangeTypeMulti( @Body() body: { profileId: string[]; type: string; isReserve: boolean; remark?: string | null }, @Request() req: RequestWithUser, ) { @@ -1392,9 +1392,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -1407,9 +1407,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFull == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFull == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFull - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -1422,9 +1422,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFullHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFullHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFullHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -1544,234 +1544,648 @@ export class SalaryPeriodController extends Controller { return new HttpSuccess(); } - //NEW CHANGE TYPE-MULTI - @Post("newchange/type-multi") - async newchangeTypeMulti( - @Body() body: { profileId: string[]; type: string; isReserve: boolean; remark?: string | null }, - @Request() req: RequestWithUser, - ) { - await new permission().PermissionCreate(req, "SYS_SALARY_OFFICER"); + // //NEW CHANGE TYPE-MULTI + // @Post("newchange/type-multi") + // async newchangeTypeMulti( + // @Body() body: { profileId: string[]; type: string; isReserve: boolean; remark?: string | null }, + // @Request() req: RequestWithUser, + // ) { + // await new permission().PermissionCreate(req, "SYS_SALARY_OFFICER"); - // ----------------------------- - // 1) ดึง salaryProfiles ทีเดียว - // ----------------------------- - const salaryProfiles = await this.salaryProfileRepository.find({ - where: { id: In(body.profileId) }, - relations: ["salaryOrg", "salaryOrg.salaryPeriod"], + // // ----------------------------- + // // 1) ดึง salaryProfiles ทีเดียว + // // ----------------------------- + // const salaryProfiles = await this.salaryProfileRepository.find({ + // where: { id: In(body.profileId) }, + // relations: ["salaryOrg", "salaryOrg.salaryPeriod"], + // }); + + // if (!salaryProfiles.length) { + // throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล salaryProfile"); + // } + + // // ----------------------------- + // // 2) โหลด PosType, PosLevel, Salary, SalaryRanks ทั้งหมดทีเดียว + // // ----------------------------- + // const posTypes = await this.posTypeRepository.find(); + // const posLevels = await this.posLevelRepository.find(); + // const salaries = await this.salaryRepository.find({ where: { isActive: true } }); + // const salaryRanks = await this.salaryRankRepository.find(); + + // // Map lookup + // const posTypeMap = new Map(posTypes.map((x) => [x.posTypeName, x])); + // const posLevelMap = new Map(posLevels.map((x) => [`${x.posTypeId}|${x.posLevelName}`, x])); + // const salaryMap = new Map( + // salaries.map((x) => [`${x.posTypeId}|${x.posLevelId}|${x.isSpecial ? 1 : 0}`, x]), + // ); + // const ranksBySalaryId = salaryRanks.reduce( + // (acc, r: any) => { + // if (!acc[r.salaryId]) acc[r.salaryId] = []; + // acc[r.salaryId].push(r); + // return acc; + // }, + // {} as Record, + // ); + + // const profilesToSave: SalaryProfile[] = []; + // const orgNeedRecalc = new Set(); + + // for (const profile of salaryProfiles) { + // const bodyType = body.type?.toUpperCase() ?? profile.type; + + // // --- ตรวจ FULLHAFT → หา APR snapshot2 --- + // if (bodyType === "FULLHAFT" && profile.salaryOrg.salaryPeriod.period === "OCT") { + // const checkPrev = await this.salaryProfileRepository.findOne({ + // relations: ["salaryOrg", "salaryOrg.salaryPeriod"], + // where: { + // citizenId: profile.citizenId, + // salaryOrg: { + // salaryPeriod: { period: "APR", year: profile.salaryOrg.salaryPeriod.year }, + // snapshot: "SNAP2", + // }, + // type: "FULL", + // }, + // }); + + // if (checkPrev) { + // throw new HttpError(404, "ไม่สามารถเลื่อนขั้นเกิน 2 ครั้งในปีเดียวกันได้"); + // } + // } + + // // --- apply posType/posLevel/salary --- + // const posType = posTypeMap.get(profile.posType); + // if (!posType) throw new HttpError(404, "ไม่พบประเภทตำแหน่ง"); + + // const posLevel = posLevelMap.get(`${posType.id}|${profile.posLevel}`); + // if (!posLevel) throw new HttpError(404, "ไม่พบระดับตำแหน่ง"); + + // const salaryBase = salaryMap.get( + // `${posLevel.posTypeId}|${posLevel.id}|${profile.isSpecial ? 1 : 0}`, + // ); + // if (!salaryBase) throw new HttpError(404, "ไม่พบระดับเงินเดือน"); + + // const salaryId = Number(salaryBase.id); + // const ranks = ranksBySalaryId[salaryId] ?? []; + + // // --- หา rank ตาม amount ก่อน dynamic type adjustment --- + // let rank: SalaryRanks | null = null; + // if (profile.amount != null) { + // const amount = profile.amount; + + // const possible = ranks + // .filter((r: any) => r.salary >= amount && !r.isNext) + // .sort((a: any, b: any) => a.salary - b.salary); + // rank = possible[0] ?? null; + + // if (!rank) { + // const next = ranks + // .filter((r: any) => r.salary > amount && r.isNext) + // .sort((a: any, b: any) => a.salary - b.salary); + // rank = next[0] ?? null; + // } + // } + + // // --- คำนวณเงินเดือนตาม rank เดิมก่อน dynamic type adjustment --- + // const calc = (sp: keyof SalaryRanks, next: keyof SalaryRanks) => ({ + // amountSpecial: rank?.[sp] ?? 0, + // amountUse: + // profile.amount != null && rank?.[next] != null + // ? Number(rank[next]) - Number(profile.amount) + // : 0, + // positionSalaryAmount: rank?.[next] ?? 0, + // isNext: rank?.isNext ?? 0, + // }); + + // // --- FULLHAFT dynamic type adjustment หลังคำนวณเงินเดือน --- + // let finalType = bodyType; + // if (bodyType === "FULLHAFT" && rank) { + // const halfSpecial = rank.salaryHalfSpecial ?? 0; + // const fullSpecial = rank.salaryFullSpecial ?? 0; + // const fullHalfSpecial = rank.salaryFullHalfSpecial ?? 0; + + // if (fullHalfSpecial > 0) { + // if (fullSpecial === 0) finalType = "HAFT"; + // else if (halfSpecial === 0) finalType = "FULL"; + // else finalType = "FULLHAFT"; + // } + // } + + // profile.type = finalType; + // profile.isReserve = finalType === "FULL" ? body.isReserve : false; + // profile.remark = body.remark ?? ""; + + // if (finalType === "NONE") { + // profile.amountSpecial = 0; + // profile.amountUse = 0; + // profile.positionSalaryAmount = profile.amount ?? 0; + // } else if (finalType === "PENDING") { + // profile.amountSpecial = 0; + // profile.amountUse = 0; + // profile.positionSalaryAmount = 0; + // } else if (finalType === "HAFT") { + // Object.assign(profile, calc("salaryHalfSpecial", "salaryHalf")); + // } else if (finalType === "FULL") { + // Object.assign(profile, calc("salaryFullSpecial", "salaryFull")); + // } else if (finalType === "FULLHAFT") { + // Object.assign(profile, calc("salaryFullHalfSpecial", "salaryFullHalf")); + // } + + // profile.lastUpdateUserId = req.user.sub; + // profile.lastUpdateFullName = req.user.name; + // profile.lastUpdatedAt = new Date(); + + // // --- log diff --- + // const before = structuredClone(profile); + // profilesToSave.push(profile); + // setLogDataDiff(req, { before, after: profile }); + + // orgNeedRecalc.add(profile.salaryOrg.id); + // } + + // // ----------------------------- + // // 4) Save batch + // // ----------------------------- + // await this.salaryProfileRepository.save(profilesToSave); + + // // ----------------------------- + // // 5) Recalculate SalaryOrg + // // ----------------------------- + // for (const orgId of orgNeedRecalc) { + // const org = await this.salaryOrgRepository.findOne({ + // where: { id: orgId }, + // relations: ["salaryProfiles", "salaryPeriod"], + // }); + // if (!org) continue; + + // const beforeOrg = structuredClone(org); + + // // SNAP1 / APR + // if (org.snapshot === "SNAP1" && org.salaryPeriod.period === "APR") { + // const countFull = org.salaryProfiles.filter((p) => p.type === "FULL").length; + // org.total = org.salaryProfiles.length; + // org.fifteenPercent = Math.floor(org.total * 0.15); + // org.quantityUsed = countFull; + // org.remainQuota = org.fifteenPercent - countFull; + // } + + // // SNAP1 / OCT + // if (org.snapshot === "SNAP1" && org.salaryPeriod.period === "OCT") { + // const total = org.salaryProfiles.reduce((sum, p) => sum + (p.amount ?? 0), 0); + // org.currentAmount = total; + // org.sixPercentAmount = total * 0.06; + + // const useAmount = org.salaryProfiles + // .filter((p) => ["HAFT", "FULL", "FULLHAFT"].includes(p.type)) + // .reduce((s, p) => s + (p.amountUse ?? 0), 0); + + // org.useAmount = useAmount; + // org.remainingAmount = org.sixPercentAmount - useAmount; + + // // --- SNAP2 APR recalc --- + // const salaryPeriodAPROld = await this.salaryPeriodRepository.findOne({ + // where: { period: "APR", year: org.salaryPeriod.year }, + // }); + // if (salaryPeriodAPROld) { + // const orgSnap2Old: any = await this.salaryOrgRepository.findOne({ + // where: { + // salaryPeriodId: salaryPeriodAPROld.id, + // rootId: org.rootId, + // group: org.group, + // snapshot: "SNAP2", + // }, + // relations: ["salaryProfiles"], + // }); + // if (orgSnap2Old) { + // const spent = orgSnap2Old.salaryProfiles.reduce( + // (sum: number, p: any) => sum + (p.amountUse ?? 0), + // 0, + // ); + // org.spentAmount = spent; + // org.remainingAmount = org.sixPercentAmount - useAmount - spent; + // } + // } + // } + + // org.lastUpdateUserId = req.user.sub; + // org.lastUpdateFullName = req.user.name; + // org.lastUpdatedAt = new Date(); + + // await this.salaryOrgRepository.save(org); + // setLogDataDiff(req, { before: beforeOrg, after: org }); + // } + + // return new HttpSuccess(); + // } + + private chunkArray(arr: T[], size: number): T[][] { + const result: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); + } + return result; + } + + private async recalculateSalaryOrg( + orgId: string, + req: RequestWithUser, + ) { + const salaryOrg = await this.salaryOrgRepository.findOne({ + where: { id: orgId }, + relations: ["salaryProfiles", "salaryPeriod"], }); - if (!salaryProfiles.length) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล salaryProfile"); + if (!salaryOrg) return; + + // ===== เฉพาะ SNAP1 เท่านั้น (ตาม original) ===== + if (salaryOrg.snapshot !== "SNAP1") return; + + // ===================== APR ===================== + if (salaryOrg.salaryPeriod.period === "APR") { + const amountFullType = await this.salaryProfileRepository.count({ + where: { + salaryOrgId: salaryOrg.id, + type: "FULL", + }, + }); + + salaryOrg.total = salaryOrg.salaryProfiles.length; + salaryOrg.fifteenPercent = Math.floor( + (salaryOrg.salaryProfiles.length * 15) / 100, + ); + salaryOrg.fifteenPoint = + (salaryOrg.salaryProfiles.length * 15) % 100; + salaryOrg.quantityUsed = amountFullType; + + const calRemainQuota = + salaryOrg.fifteenPercent - amountFullType; + salaryOrg.remainQuota = calRemainQuota; + + salaryOrg.lastUpdateUserId = req.user.sub; + salaryOrg.lastUpdateFullName = req.user.name; + salaryOrg.lastUpdatedAt = new Date(); + + await this.salaryOrgRepository.save(salaryOrg, { data: req }); + return; } - // ----------------------------- - // 2) โหลด PosType, PosLevel, Salary, SalaryRanks ทั้งหมดทีเดียว - // ----------------------------- - const posTypes = await this.posTypeRepository.find(); - const posLevels = await this.posLevelRepository.find(); - const salaries = await this.salaryRepository.find({ where: { isActive: true } }); - const salaryRanks = await this.salaryRankRepository.find(); + // ===================== OCT ===================== + if (salaryOrg.salaryPeriod.period === "OCT") { + // ===== currentAmount / total ===== + const totalProfileAmount = Extension.sumObjectValues( + salaryOrg.salaryProfiles, + "amount", + ); - // Map lookup - const posTypeMap = new Map(posTypes.map((x) => [x.posTypeName, x])); - const posLevelMap = new Map(posLevels.map((x) => [`${x.posTypeId}|${x.posLevelName}`, x])); - const salaryMap = new Map( - salaries.map((x) => [`${x.posTypeId}|${x.posLevelId}|${x.isSpecial ? 1 : 0}`, x]), - ); - const ranksBySalaryId = salaryRanks.reduce( - (acc, r: any) => { - if (!acc[r.salaryId]) acc[r.salaryId] = []; - acc[r.salaryId].push(r); - return acc; - }, - {} as Record, - ); + salaryOrg.currentAmount = totalProfileAmount; + salaryOrg.total = salaryOrg.salaryProfiles.length; + salaryOrg.sixPercentAmount = totalProfileAmount * 0.06; - const profilesToSave: SalaryProfile[] = []; - const orgNeedRecalc = new Set(); + // ===== spentAmount (APR SNAP2) ===== + let totalAmount = 0; - for (const profile of salaryProfiles) { - const bodyType = body.type?.toUpperCase() ?? profile.type; + const salaryPeriodAPROld = + await this.salaryPeriodRepository.findOne({ + where: { + year: salaryOrg.salaryPeriod.year, + period: "APR", + }, + }); - // --- ตรวจ FULLHAFT → หา APR snapshot2 --- - if (bodyType === "FULLHAFT" && profile.salaryOrg.salaryPeriod.period === "OCT") { - const checkPrev = await this.salaryProfileRepository.findOne({ + if (salaryPeriodAPROld) { + const salaryOrgSnap2Old = + await this.salaryOrgRepository.findOne({ + where: { + salaryPeriodId: salaryPeriodAPROld.id, + rootId: salaryOrg.rootId, + group: salaryOrg.group, + snapshot: "SNAP2", + }, + relations: ["salaryProfiles"], + }); + + totalAmount = + salaryOrgSnap2Old == null + ? 0 + : Extension.sumObjectValues( + salaryOrgSnap2Old.salaryProfiles, + "amountUse", + ); + } + + salaryOrg.spentAmount = totalAmount; + + // ===== sumAmountUse (current OCT SNAP1) ===== + const sumAmountUse = + await this.salaryProfileRepository + .createQueryBuilder("salaryProfile") + .select( + "SUM(salaryProfile.amountUse)", + "totalAmount", + ) + .where({ + salaryOrgId: salaryOrg.id, + type: In(["HAFT", "FULL", "FULLHAFT"]), + }) + .getRawOne(); + + salaryOrg.useAmount = + sumAmountUse == null || + sumAmountUse.totalAmount == null + ? 0 + : sumAmountUse.totalAmount; + + const calRemainAmount = + salaryOrg.sixPercentAmount - + salaryOrg.useAmount - + salaryOrg.spentAmount; + + salaryOrg.remainingAmount = calRemainAmount; + + salaryOrg.lastUpdateUserId = req.user.sub; + salaryOrg.lastUpdateFullName = req.user.name; + salaryOrg.lastUpdatedAt = new Date(); + + await this.salaryOrgRepository.save(salaryOrg, { data: req }); + return; + } + } + + + private async processSingleProfile( + profileId: string, + body: any, + req: RequestWithUser, + posTypeCache: Map, + posLevelCache: Map, + salaryCache: Map, + affectedOrgIds: Set, + ) { + const salaryProfile = await this.salaryProfileRepository.findOne({ + relations: ["salaryOrg", "salaryOrg.salaryPeriod"], + where: { id: profileId }, + }); + + if (!salaryProfile) { + throw new HttpError( + HttpStatusCode.NOT_FOUND, + "ไม่พบข้อมูลการขอเงินเดือนผู้ใช้งานนี้ในระบบ", + ); + } + + // ==== CHECK FULLHAFT เดิม ==== + if (body.type === "FULLHAFT") { + if (salaryProfile.salaryOrg.salaryPeriod.period === "OCT") { + const checkPreviousType = await this.salaryProfileRepository.findOne({ relations: ["salaryOrg", "salaryOrg.salaryPeriod"], where: { - citizenId: profile.citizenId, + citizenId: salaryProfile.citizenId, salaryOrg: { - salaryPeriod: { period: "APR", year: profile.salaryOrg.salaryPeriod.year }, + salaryPeriod: { + period: "APR", + year: salaryProfile.salaryOrg.salaryPeriod.year, + }, snapshot: "SNAP2", }, type: "FULL", }, }); - if (checkPrev) { - throw new HttpError(404, "ไม่สามารถเลื่อนขั้นเกิน 2 ครั้งในปีเดียวกันได้"); + if (checkPreviousType) { + throw new HttpError( + HttpStatusCode.NOT_FOUND, + "ไม่สามารถเลื่อนขั้นบุคลากรเกิน 2 ขั้นต่อปีได้", + ); } } - - // --- apply posType/posLevel/salary --- - const posType = posTypeMap.get(profile.posType); - if (!posType) throw new HttpError(404, "ไม่พบประเภทตำแหน่ง"); - - const posLevel = posLevelMap.get(`${posType.id}|${profile.posLevel}`); - if (!posLevel) throw new HttpError(404, "ไม่พบระดับตำแหน่ง"); - - const salaryBase = salaryMap.get( - `${posLevel.posTypeId}|${posLevel.id}|${profile.isSpecial ? 1 : 0}`, - ); - if (!salaryBase) throw new HttpError(404, "ไม่พบระดับเงินเดือน"); - - const salaryId = Number(salaryBase.id); - const ranks = ranksBySalaryId[salaryId] ?? []; - - // --- หา rank ตาม amount ก่อน dynamic type adjustment --- - let rank: SalaryRanks | null = null; - if (profile.amount != null) { - const amount = profile.amount; - - const possible = ranks - .filter((r: any) => r.salary >= amount && !r.isNext) - .sort((a: any, b: any) => a.salary - b.salary); - rank = possible[0] ?? null; - - if (!rank) { - const next = ranks - .filter((r: any) => r.salary > amount && r.isNext) - .sort((a: any, b: any) => a.salary - b.salary); - rank = next[0] ?? null; - } - } - - // --- คำนวณเงินเดือนตาม rank เดิมก่อน dynamic type adjustment --- - const calc = (sp: keyof SalaryRanks, next: keyof SalaryRanks) => ({ - amountSpecial: rank?.[sp] ?? 0, - amountUse: - profile.amount != null && rank?.[next] != null - ? Number(rank[next]) - Number(profile.amount) - : 0, - positionSalaryAmount: rank?.[next] ?? 0, - isNext: rank?.isNext ?? 0, - }); - - // --- FULLHAFT dynamic type adjustment หลังคำนวณเงินเดือน --- - let finalType = bodyType; - if (bodyType === "FULLHAFT" && rank) { - const halfSpecial = rank.salaryHalfSpecial ?? 0; - const fullSpecial = rank.salaryFullSpecial ?? 0; - const fullHalfSpecial = rank.salaryFullHalfSpecial ?? 0; - - if (fullHalfSpecial > 0) { - if (fullSpecial === 0) finalType = "HAFT"; - else if (halfSpecial === 0) finalType = "FULL"; - else finalType = "FULLHAFT"; - } - } - - profile.type = finalType; - profile.isReserve = finalType === "FULL" ? body.isReserve : false; - profile.remark = body.remark ?? ""; - - if (finalType === "NONE") { - profile.amountSpecial = 0; - profile.amountUse = 0; - profile.positionSalaryAmount = profile.amount ?? 0; - } else if (finalType === "PENDING") { - profile.amountSpecial = 0; - profile.amountUse = 0; - profile.positionSalaryAmount = 0; - } else if (finalType === "HAFT") { - Object.assign(profile, calc("salaryHalfSpecial", "salaryHalf")); - } else if (finalType === "FULL") { - Object.assign(profile, calc("salaryFullSpecial", "salaryFull")); - } else if (finalType === "FULLHAFT") { - Object.assign(profile, calc("salaryFullHalfSpecial", "salaryFullHalf")); - } - - profile.lastUpdateUserId = req.user.sub; - profile.lastUpdateFullName = req.user.name; - profile.lastUpdatedAt = new Date(); - - // --- log diff --- - const before = structuredClone(profile); - profilesToSave.push(profile); - setLogDataDiff(req, { before, after: profile }); - - orgNeedRecalc.add(profile.salaryOrg.id); } - // ----------------------------- - // 4) Save batch - // ----------------------------- - await this.salaryProfileRepository.save(profilesToSave); + // ==== isReserve เดิม ==== + salaryProfile.isReserve = body.type === "FULL" ? body.isReserve : false; - // ----------------------------- - // 5) Recalculate SalaryOrg - // ----------------------------- - for (const orgId of orgNeedRecalc) { - const org = await this.salaryOrgRepository.findOne({ - where: { id: orgId }, - relations: ["salaryProfiles", "salaryPeriod"], + // ==== POS TYPE (cache) ==== + let Type = posTypeCache.get(salaryProfile.posType); + if (!Type) { + Type = await this.posTypeRepository.findOne({ + where: { posTypeName: salaryProfile.posType }, }); - if (!org) continue; - - const beforeOrg = structuredClone(org); - - // SNAP1 / APR - if (org.snapshot === "SNAP1" && org.salaryPeriod.period === "APR") { - const countFull = org.salaryProfiles.filter((p) => p.type === "FULL").length; - org.total = org.salaryProfiles.length; - org.fifteenPercent = Math.floor(org.total * 0.15); - org.quantityUsed = countFull; - org.remainQuota = org.fifteenPercent - countFull; + if (!Type) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทตำแหน่ง"); } + posTypeCache.set(salaryProfile.posType, Type); + } - // SNAP1 / OCT - if (org.snapshot === "SNAP1" && org.salaryPeriod.period === "OCT") { - const total = org.salaryProfiles.reduce((sum, p) => sum + (p.amount ?? 0), 0); - org.currentAmount = total; - org.sixPercentAmount = total * 0.06; + // ==== POS LEVEL (cache) ==== + const levelKey = `${Type.id}_${salaryProfile.posLevel}`; + let Level = posLevelCache.get(levelKey); + if (!Level) { + Level = await this.posLevelRepository.findOne({ + where: { + posTypeId: Type.id, + posLevelName: salaryProfile.posLevel, + }, + }); + if (!Level) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบระดับตำแหน่ง"); + } + posLevelCache.set(levelKey, Level); + } - const useAmount = org.salaryProfiles - .filter((p) => ["HAFT", "FULL", "FULLHAFT"].includes(p.type)) - .reduce((s, p) => s + (p.amountUse ?? 0), 0); + // ==== SALARY (cache) ==== + const salaryKey = `${Level.posTypeId}_${Level.id}_${salaryProfile.isSpecial}`; + let salarys = salaryCache.get(salaryKey); + if (!salarys) { + salarys = await this.salaryRepository.findOne({ + where: { + posTypeId: Level.posTypeId, + posLevelId: Level.id, + isSpecial: salaryProfile.isSpecial === true, + isActive: true, + }, + }); + if (!salarys) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบระดับตำแหน่ง"); + } + salaryCache.set(salaryKey, salarys); + } - org.useAmount = useAmount; - org.remainingAmount = org.sixPercentAmount - useAmount; + // ====== ตั้งค่า type / remark ====== + salaryProfile.type = body.type; + salaryProfile.remark = body.remark == null ? null : body.remark; - // --- SNAP2 APR recalc --- - const salaryPeriodAPROld = await this.salaryPeriodRepository.findOne({ - where: { period: "APR", year: org.salaryPeriod.year }, - }); - if (salaryPeriodAPROld) { - const orgSnap2Old: any = await this.salaryOrgRepository.findOne({ - where: { - salaryPeriodId: salaryPeriodAPROld.id, - rootId: org.rootId, - group: org.group, - snapshot: "SNAP2", - }, - relations: ["salaryProfiles"], - }); - if (orgSnap2Old) { - const spent = orgSnap2Old.salaryProfiles.reduce( - (sum: number, p: any) => sum + (p.amountUse ?? 0), - 0, - ); - org.spentAmount = spent; - org.remainingAmount = org.sixPercentAmount - useAmount - spent; + let type = salaryProfile.type; + let salaryRanks: any = null; + + // ====== SALARY RANK (logic เดิม 100%) ====== + if (salaryProfile.amount != null) { + salaryRanks = await this.salaryRankRepository.findOne({ + where: { + salaryId: salarys.id, + salary: MoreThanOrEqual(salaryProfile.amount), + isNext: false, + }, + order: { salary: "ASC" }, + }); + + if (salaryRanks != null) { + if (salaryProfile.type === "HAFT") { + if (salaryRanks.salaryHalfSpecial != null && salaryRanks.salaryHalfSpecial > 0) { + const _salaryRanks = await this.salaryRankRepository.findOne({ + where: { + salaryId: salarys.id, + salary: salaryRanks.salaryHalf, + isNext: true, + }, + }); + salaryRanks = _salaryRanks ?? salaryRanks; + } + } else if (salaryProfile.type === "FULL") { + if (salaryRanks.salaryFullSpecial != null && salaryRanks.salaryFullSpecial > 0) { + if (!salaryRanks.salaryHalfSpecial) { + type = "HAFT"; + } + const _salaryRanks = await this.salaryRankRepository.findOne({ + where: { + salaryId: salarys.id, + salary: salaryRanks.salaryFull, + isNext: true, + }, + }); + salaryRanks = _salaryRanks ?? salaryRanks; + } + } else if (salaryProfile.type === "FULLHAFT") { + if (salaryRanks.salaryFullHalfSpecial != null && salaryRanks.salaryFullHalfSpecial > 0) { + if (!salaryRanks.salaryFullSpecial) { + type = "HAFT"; + } else if (!salaryRanks.salaryHalfSpecial) { + type = "FULL"; + } + const _salaryRanks = await this.salaryRankRepository.findOne({ + where: { + salaryId: salarys.id, + salary: salaryRanks.salaryFullHalf, + isNext: true, + }, + }); + salaryRanks = _salaryRanks ?? salaryRanks; } } + } else { + salaryRanks = await this.salaryRankRepository.findOne({ + where: { + salaryId: salarys.id, + salary: salaryProfile.amount, + isNext: true, + }, + }); + + if (!salaryRanks) { + salaryRanks = await this.salaryRankRepository.findOne({ + where: { + salaryId: salarys.id, + salary: MoreThan(salaryProfile.amount), + isNext: true, + }, + order: { salary: "ASC" }, + }); + } } + } - org.lastUpdateUserId = req.user.sub; - org.lastUpdateFullName = req.user.name; - org.lastUpdatedAt = new Date(); + // ====== SET RESULT (logic เดิม) ====== + salaryProfile.isNext = false; - await this.salaryOrgRepository.save(org); - setLogDataDiff(req, { before: beforeOrg, after: org }); + if (type === "NONE") { + salaryProfile.amountSpecial = 0; + salaryProfile.amountUse = 0; + salaryProfile.positionSalaryAmount = salaryProfile.amount ?? 0; + } else if (type === "PENDING") { + salaryProfile.amountSpecial = 0; + salaryProfile.amountUse = 0; + salaryProfile.positionSalaryAmount = 0; + } else if (type === "HAFT") { + salaryProfile.amountSpecial = salaryRanks?.salaryHalfSpecial ?? 0; + salaryProfile.amountUse = + salaryRanks && salaryRanks.salaryHalf && salaryProfile.amount + ? salaryRanks.salaryHalf - salaryProfile.amount + : 0; + salaryProfile.positionSalaryAmount = salaryRanks?.salaryHalf ?? 0; + salaryProfile.isNext = salaryRanks?.isNext ?? false; + } else if (type === "FULL") { + salaryProfile.amountSpecial = salaryRanks?.salaryFullSpecial ?? 0; + salaryProfile.amountUse = + salaryRanks && salaryRanks.salaryFull && salaryProfile.amount + ? salaryRanks.salaryFull - salaryProfile.amount + : 0; + salaryProfile.positionSalaryAmount = salaryRanks?.salaryFull ?? 0; + salaryProfile.isNext = salaryRanks?.isNext ?? false; + } else if (type === "FULLHAFT") { + salaryProfile.amountSpecial = salaryRanks?.salaryFullHalfSpecial ?? 0; + salaryProfile.amountUse = + salaryRanks && salaryRanks.salaryFullHalf && salaryProfile.amount + ? salaryRanks.salaryFullHalf - salaryProfile.amount + : 0; + salaryProfile.positionSalaryAmount = salaryRanks?.salaryFullHalf ?? 0; + salaryProfile.isNext = salaryRanks?.isNext ?? false; + } else { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ประเภทการเลื่อนขึ้นเงินเดือนไม่ถูกต้อง"); + } + + salaryProfile.lastUpdateUserId = req.user.sub; + salaryProfile.lastUpdateFullName = req.user.name; + salaryProfile.lastUpdatedAt = new Date(); + + const before = structuredClone(salaryProfile); + await this.salaryProfileRepository.save(salaryProfile, { data: req }); + setLogDataDiff(req, { before, after: salaryProfile }); + + affectedOrgIds.add(salaryProfile.salaryOrg.id); + } + + @Post("change/type-multi") + async changeTypeMulti( + @Body() + body: { + profileId: string[]; + type: string; + isReserve: boolean; + remark?: string | null; + }, + @Request() req: RequestWithUser, + ) { + await new permission().PermissionCreate(req, "SYS_SALARY_OFFICER"); + + body.type = body.type.toUpperCase(); + + const BATCH_SIZE = 20; + const batches = this.chunkArray(body.profileId, BATCH_SIZE); + + // cache + const posTypeCache = new Map(); + const posLevelCache = new Map(); + const salaryCache = new Map(); + + // org ที่ต้อง recalc + const affectedOrgIds = new Set(); + + for (const batch of batches) { + await Promise.all( + batch.map(profileId => + this.processSingleProfile( + profileId, + body, + req, + posTypeCache, + posLevelCache, + salaryCache, + affectedOrgIds, + ), + ), + ); + } + + // recalc salaryOrg ทีเดียวต่อ org + for (const orgId of affectedOrgIds) { + await this.recalculateSalaryOrg(orgId, req); } return new HttpSuccess(); @@ -2172,9 +2586,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -2187,9 +2601,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFull == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFull == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFull - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -2202,9 +2616,9 @@ export class SalaryPeriodController extends Controller { : salaryRanks.salaryFullHalfSpecial; salaryProfile.amountUse = salaryRanks == null || - salaryProfile == null || - salaryRanks.salaryFullHalf == null || - salaryProfile.amount == null + salaryProfile == null || + salaryRanks.salaryFullHalf == null || + salaryProfile.amount == null ? 0 : salaryRanks.salaryFullHalf - salaryProfile.amount; salaryProfile.positionSalaryAmount = @@ -3024,27 +3438,27 @@ export class SalaryPeriodController extends Controller { const [salaryProfileCount, salaryProfileEmployeeCount] = await Promise.all([ salaryOrgCount > 0 ? AppDataSource.query( - ` + ` SELECT COUNT(*) as count FROM salaryProfile WHERE salaryOrgId IN ( SELECT id FROM salaryOrg WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ).then((result) => result[0]?.count || 0) + [salaryPeriod.id, snapshot], + ).then((result) => result[0]?.count || 0) : Promise.resolve(0), salaryOrgEmployeeCount > 0 ? AppDataSource.query( - ` + ` SELECT COUNT(*) as count FROM salaryProfileEmployee WHERE salaryOrgId IN ( SELECT id FROM salaryOrgEmployee WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ).then((result) => result[0]?.count || 0) + [salaryPeriod.id, snapshot], + ).then((result) => result[0]?.count || 0) : Promise.resolve(0), ]); @@ -3057,27 +3471,27 @@ export class SalaryPeriodController extends Controller { await Promise.all([ salaryOrgCount > 0 ? AppDataSource.query( - ` + ` DELETE FROM salaryProfile WHERE salaryOrgId IN ( SELECT id FROM salaryOrg WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ) + [salaryPeriod.id, snapshot], + ) : Promise.resolve(), salaryOrgEmployeeCount > 0 ? AppDataSource.query( - ` + ` DELETE FROM salaryProfileEmployee WHERE salaryOrgId IN ( SELECT id FROM salaryOrgEmployee WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ) + [salaryPeriod.id, snapshot], + ) : Promise.resolve(), ]); @@ -4053,27 +4467,27 @@ export class SalaryPeriodController extends Controller { const [salaryProfileCount, salaryProfileEmployeeCount] = await Promise.all([ salaryOrgCount > 0 ? AppDataSource.query( - ` + ` SELECT COUNT(*) as count FROM salaryProfile WHERE salaryOrgId IN ( SELECT id FROM salaryOrg WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ).then((result) => result[0]?.count || 0) + [salaryPeriod.id, snapshot], + ).then((result) => result[0]?.count || 0) : Promise.resolve(0), salaryOrgEmployeeCount > 0 ? AppDataSource.query( - ` + ` SELECT COUNT(*) as count FROM salaryProfileEmployee WHERE salaryOrgId IN ( SELECT id FROM salaryOrgEmployee WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ).then((result) => result[0]?.count || 0) + [salaryPeriod.id, snapshot], + ).then((result) => result[0]?.count || 0) : Promise.resolve(0), ]); @@ -4086,27 +4500,27 @@ export class SalaryPeriodController extends Controller { await Promise.all([ salaryOrgCount > 0 ? AppDataSource.query( - ` + ` DELETE FROM salaryProfile WHERE salaryOrgId IN ( SELECT id FROM salaryOrg WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ) + [salaryPeriod.id, snapshot], + ) : Promise.resolve(), salaryOrgEmployeeCount > 0 ? AppDataSource.query( - ` + ` DELETE FROM salaryProfileEmployee WHERE salaryOrgId IN ( SELECT id FROM salaryOrgEmployee WHERE salaryPeriodId = ? AND snapshot = ? ) `, - [salaryPeriod.id, snapshot], - ) + [salaryPeriod.id, snapshot], + ) : Promise.resolve(), ]); From 83964e1198a103c16d48ea81971598bceaa8c469 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 19 Jan 2026 14:26:56 +0700 Subject: [PATCH 2/3] test script emp changetype-mutl --- .../SalaryPeriodEmployeeController.ts | 420 ++++++++++++++---- 1 file changed, 339 insertions(+), 81 deletions(-) diff --git a/src/controllers/SalaryPeriodEmployeeController.ts b/src/controllers/SalaryPeriodEmployeeController.ts index 1f1abae..88120ab 100644 --- a/src/controllers/SalaryPeriodEmployeeController.ts +++ b/src/controllers/SalaryPeriodEmployeeController.ts @@ -70,68 +70,68 @@ export class SalaryPeriodEmployeeController extends Controller { const data = { group1id: salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.id, + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.id, group1IsClose: salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP1" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.isClose, + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP1" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.isClose, group2id: salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.id, + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.id, group2IsClose: salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - ) == null + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + ) == null ? null : salaryPeriod.salaryOrgEmployees && - salaryPeriod.salaryOrgEmployees.find( - (x) => - x.group == "GROUP2" && - x.rootId == body.rootId && - x.snapshot == body.snapshot.toLocaleUpperCase(), - )?.isClose, + salaryPeriod.salaryOrgEmployees.find( + (x) => + x.group == "GROUP2" && + x.rootId == body.rootId && + x.snapshot == body.snapshot.toLocaleUpperCase(), + )?.isClose, effectiveDate: salaryPeriod.effectiveDate, period: salaryPeriod.period, }; @@ -762,7 +762,7 @@ export class SalaryPeriodEmployeeController extends Controller { let type = salaryProfile.type; salaryProfile = await this.calSalaryNew(type, salaryProfile); - + salaryProfile.lastUpdateUserId = req.user.sub; salaryProfile.lastUpdateFullName = req.user.name; salaryProfile.lastUpdatedAt = new Date(); @@ -879,8 +879,8 @@ export class SalaryPeriodEmployeeController extends Controller { * @param {string} id profile Id * @param {string} type ประเภทการเลื่อน NONE->ไม่ได้เลื่อน HAFT->ครึ่งขั้น FULL->1ขั้น FULLHAFT->1.5ขั้น */ - @Post("change/type-multi") - async changeTypeMulti( + @Post("oldchange/type-multi") + async oldchangeTypeMulti( @Body() body: { profileId: string[]; type: string; isReserve: boolean; remark?: string | null }, @Request() req: RequestWithUser, ) { @@ -1063,6 +1063,264 @@ export class SalaryPeriodEmployeeController extends Controller { return new HttpSuccess(); } + private chunkArray(arr: T[], size: number): T[][] { + const result: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); + } + return result; + } + + private async processEmployeeProfile( + profileId: string, + body: any, + req: RequestWithUser, + affectedOrgIds: Set, + ) { + let salaryProfile = await this.salaryProfileRepository.findOne({ + relations: ["salaryOrg", "salaryOrg.salaryPeriod"], + where: { id: profileId }, + }); + + if (!salaryProfile) { + throw new HttpError( + HttpStatusCode.NOT_FOUND, + "ไม่พบข้อมูลการขอเงินเดือนผู้ใช้งานนี้ในระบบ", + ); + } + + // ===== FULLHAFT CHECK (เดิม) ===== + if (body.type === "FULLHAFT") { + if (salaryProfile.salaryOrg.salaryPeriod.period === "OCT") { + const checkPreviousType = + await this.salaryProfileRepository.findOne({ + relations: ["salaryOrg", "salaryOrg.salaryPeriod"], + where: { + citizenId: salaryProfile.citizenId, + salaryOrg: { + salaryPeriod: { + period: "APR", + year: salaryProfile.salaryOrg.salaryPeriod.year, + }, + snapshot: "SNAP2", + }, + type: "FULL", + }, + }); + + if (checkPreviousType) { + throw new HttpError( + HttpStatusCode.NOT_FOUND, + "ไม่สามารถเลื่อนขั้นบุคลากรเกิน 2 ขั้นต่อปีได้", + ); + } + } + } + + // ===== isReserve (เดิม) ===== + salaryProfile.isReserve = + body.type === "FULL" ? body.isReserve : false; + + // ===== Type & Level check (เดิม) ===== + const Type = await this.posTypeRepository.findOne({ + where: { posTypeName: salaryProfile.posType }, + }); + if (!Type) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทตำแหน่ง"); + } + + const Level = await this.posLevelRepository.findOne({ + where: { + posTypeId: Type.id, + posLevelName: salaryProfile.posLevel, + }, + }); + if (!Level) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบระดับตำแหน่ง"); + } + + // ===== type / remark ===== + salaryProfile.type = body.type; + salaryProfile.remark = body.remark == null ? null : body.remark; + + // ===== CALC SALARY (เดิม 100%) ===== + salaryProfile = await this.calSalaryNew( + salaryProfile.type, + salaryProfile, + ); + + // ===== audit ===== + salaryProfile.lastUpdateUserId = req.user.sub; + salaryProfile.lastUpdateFullName = req.user.name; + salaryProfile.lastUpdatedAt = new Date(); + + const before = structuredClone(salaryProfile); + await this.salaryProfileRepository.save(salaryProfile, { + data: req, + }); + setLogDataDiff(req, { before, after: salaryProfile }); + + affectedOrgIds.add(salaryProfile.salaryOrg.id); + } + + private async recalculateSalaryOrgEmployee( + orgId: string, + req: RequestWithUser, + ) { + const salaryOrg = await this.salaryOrgRepository.findOne({ + where: { id: orgId }, + relations: ["salaryProfiles", "salaryPeriod"], + }); + + if (!salaryOrg) return; + if (salaryOrg.snapshot !== "SNAP1") return; + + // ===================== APR ===================== + if (salaryOrg.salaryPeriod.period === "APR") { + const amountFullType = + await this.salaryProfileRepository.count({ + where: { + salaryOrgId: salaryOrg.id, + type: "FULL", + }, + }); + + salaryOrg.total = salaryOrg.salaryProfiles.length; + salaryOrg.fifteenPercent = Math.floor( + (salaryOrg.total * 15) / 100, + ); + salaryOrg.fifteenPoint = (salaryOrg.total * 15) % 100; + salaryOrg.quantityUsed = amountFullType; + salaryOrg.remainQuota = + salaryOrg.fifteenPercent - amountFullType; + + salaryOrg.lastUpdateUserId = req.user.sub; + salaryOrg.lastUpdateFullName = req.user.name; + salaryOrg.lastUpdatedAt = new Date(); + + await this.salaryOrgRepository.save(salaryOrg, { data: req }); + return; + } + + // ===================== OCT ===================== + if (salaryOrg.salaryPeriod.period === "OCT") { + const totalProfileAmount = Extension.sumObjectValues( + salaryOrg.salaryProfiles, + "amount", + ); + + salaryOrg.currentAmount = totalProfileAmount; + salaryOrg.total = salaryOrg.salaryProfiles.length; + salaryOrg.sixPercentAmount = totalProfileAmount * 0.06; + + // ===== APR SNAP2 spentAmount ===== + let totalAmount = 0; + + const salaryPeriodAPROld = + await this.salaryPeriodRepository.findOne({ + where: { + year: salaryOrg.salaryPeriod.year, + period: "APR", + }, + }); + + if (salaryPeriodAPROld) { + const salaryOrgSnap2Old = + await this.salaryOrgRepository.findOne({ + where: { + salaryPeriodId: salaryPeriodAPROld.id, + rootId: salaryOrg.rootId, + group: salaryOrg.group, + snapshot: "SNAP2", + }, + relations: ["salaryProfiles"], + }); + + totalAmount = + salaryOrgSnap2Old == null + ? 0 + : Extension.sumObjectValues( + salaryOrgSnap2Old.salaryProfiles, + "amountUse", + ); + } + + salaryOrg.spentAmount = totalAmount; + + // ===== sumAmountUse (current OCT SNAP1) ===== + const sumAmountUse = + await AppDataSource.getRepository( + SalaryProfileEmployee, + ) + .createQueryBuilder("salaryProfileEmployee") + .select( + "SUM(salaryProfileEmployee.amountUse)", + "totalAmount", + ) + .where({ + salaryOrgId: salaryOrg.id, + type: In(["HAFT", "FULL", "FULLHAFT"]), + }) + .getRawOne(); + + salaryOrg.useAmount = + sumAmountUse?.totalAmount ?? 0; + + salaryOrg.remainingAmount = + salaryOrg.sixPercentAmount - + salaryOrg.useAmount - + salaryOrg.spentAmount; + + salaryOrg.lastUpdateUserId = req.user.sub; + salaryOrg.lastUpdateFullName = req.user.name; + salaryOrg.lastUpdatedAt = new Date(); + + await this.salaryOrgRepository.save(salaryOrg, { data: req }); + return; + } + } + + @Post("change/type-multi") + async changeTypeMulti( + @Body() + body: { + profileId: string[]; + type: string; + isReserve: boolean; + remark?: string | null; + }, + @Request() req: RequestWithUser, + ) { + await new permission().PermissionCreate(req, "SYS_WAGE"); + + body.type = body.type.toUpperCase(); + + const BATCH_SIZE = 20; + const affectedOrgIds = new Set(); + + const batches = this.chunkArray(body.profileId, BATCH_SIZE); + + for (const batch of batches) { + await Promise.all( + batch.map(profileId => + this.processEmployeeProfile( + profileId, + body, + req, + affectedOrgIds, + ), + ), + ); + } + + // === recalc salaryOrg ทีเดียวต่อ org === + for (const orgId of affectedOrgIds) { + await this.recalculateSalaryOrgEmployee(orgId, req); + } + + return new HttpSuccess(); + } + /** * API รายการอัตราเงินเดือน * @@ -1159,21 +1417,21 @@ export class SalaryPeriodEmployeeController extends Controller { }), ) - if (body.sortBy) { - if(body.sortBy === "posLevel"){ - query = query - .orderBy( `profile.posTypeShort`,body.descending ? "DESC" : "ASC") - .addOrderBy( `profile.posLevel`,body.descending ? "DESC" : "ASC"); - }else{ - query = query.orderBy( - `profile.${body.sortBy}`, - body.descending ? "DESC" : "ASC" - ); - } - }else{ - query = query.orderBy("profile.citizenId", "ASC") - .addOrderBy("profile.isReserve", "ASC") + if (body.sortBy) { + if (body.sortBy === "posLevel") { + query = query + .orderBy(`profile.posTypeShort`, body.descending ? "DESC" : "ASC") + .addOrderBy(`profile.posLevel`, body.descending ? "DESC" : "ASC"); + } else { + query = query.orderBy( + `profile.${body.sortBy}`, + body.descending ? "DESC" : "ASC" + ); } + } else { + query = query.orderBy("profile.citizenId", "ASC") + .addOrderBy("profile.isReserve", "ASC") + } const [salaryProfile, total] = await query .skip((body.page - 1) * body.pageSize) @@ -2249,7 +2507,7 @@ export class SalaryPeriodEmployeeController extends Controller { salaryProfile.positionSalaryAmountPer = 0; salaryProfile.amountSpecial = 0; } - } else { + } else { if ( salaryFormula != null && salaryFormula.salaryMax != null && @@ -2451,29 +2709,29 @@ export class SalaryPeriodEmployeeController extends Controller { }, relations: ["salaryEmployee_"], }); - + if (!salaryCurrentRanks) { salaryCurrentRanks = await this.salaryRankRepository.findOne({ where: { salaryMonth: MoreThanOrEqual(salaryProfile.amount), }, order: { - salaryMonth: "ASC", + salaryMonth: "ASC", }, relations: ["salaryEmployee_"], }); } - + if (salaryCurrentRanks) { group = salaryCurrentRanks.salaryEmployee_.group; step = salaryCurrentRanks.step; } } - //console.log("group", group); - //console.log("step", step); - - + //console.log("group", group); + //console.log("step", step); + + if (type == "HAFT") { step = step + 0.5; stepUp = 0.5; @@ -2484,7 +2742,7 @@ export class SalaryPeriodEmployeeController extends Controller { step = step + 1.5; stepUp = 1.5; } - //console.log("step+type", step); + //console.log("step+type", step); //หาขั้นสูงสุดในกลุ่มนั้น let salaryRankMax = await this.salaryRankRepository.findOne({ where: { @@ -2496,13 +2754,13 @@ export class SalaryPeriodEmployeeController extends Controller { order: { step: "DESC" }, }); - //console.log("salaryRankMax.step", salaryRankMax?.step); - //console.log("salaryProfile.amount", salaryProfile.amount); - //console.log("salaryFormula.salaryMax", salaryFormula?.salaryMax); + //console.log("salaryRankMax.step", salaryRankMax?.step); + //console.log("salaryProfile.amount", salaryProfile.amount); + //console.log("salaryFormula.salaryMax", salaryFormula?.salaryMax); //เงินเดือนเกินตาราง //****หา shot ที่ +ขั้น แล้วแก้เป็นหาเงินเดือนที่ใกล้เคียงกับขั้นผังเก่าก่อนแล้วค่อย +ขั้นที่เลื่อนเข้าไป ex.เงินตันที่ 20000 ไปหาผังใหม่ได้ใกล้เคียง 20100 ยึดตัวเลขนี้ไว้แล้วค่อย +ขั้นในผังใหม่ขึ้นไป if ( - salaryRankMax != null && + salaryRankMax != null && step > salaryRankMax.step && (salaryFormula == null || (salaryFormula != null && @@ -2510,8 +2768,8 @@ export class SalaryPeriodEmployeeController extends Controller { salaryFormula.salaryMax != null && salaryFormula.salaryMax > salaryProfile.amount)) ) { - //console.log("in function เกินตาราง"); - + //console.log("in function เกินตาราง"); + group = group + 1; //เงินเดือนในกลุ่มต่อไป let salaryRankAmount = await this.salaryRankRepository.findOne({ @@ -2533,7 +2791,7 @@ export class SalaryPeriodEmployeeController extends Controller { // (step - (salaryRankMax == null ? 0 : salaryRankMax.step) - 0.5); step = (salaryRankAmount == null ? 1 : salaryRankAmount.step) + stepUp; //****หาขั้นของผังใหม่แล้ว + ด้วยขั้นที่ได้เลื่อน - //console.log("step in if", step); + //console.log("step in if", step); } let whereCondition: any = { From 001a026e43cbf2b421f532f1908dbf1e4c2fc1cf Mon Sep 17 00:00:00 2001 From: Adisak Date: Wed, 28 Jan 2026 17:46:07 +0700 Subject: [PATCH 3/3] add: checkRootDna --- src/interfaces/permission.ts | 32 ++++++++++++++++++++++++++++++++ src/middlewares/logs.ts | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index a47c923..da284d2 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -236,6 +236,38 @@ class CheckAuth { throw error; } } + + public async checkRootDna(token: any, keycloakId: string) { + const redisClient = await this.redis.createClient({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); + try { + let reply = await getAsync("org_" + keycloakId); + if (reply != null) { + reply = JSON.parse(reply); + } else { + if (!keycloakId) throw new Error("No KeycloakId provided"); + const x = await new CallAPI().GetData( + { + headers: { authorization: token }, + }, + `/org/dotnet/user-logs/${keycloakId}`, + false, + ); + + const data = { + rootDnaId: x.rootDnaId, + }; + + return data; + } + } catch (error) { + console.error("Error calling API:", error); + throw error; + } + } public async PermissionCreate(req: RequestWithUser, system: string) { return await this.Permission(req, system, "CREATE"); } diff --git a/src/middlewares/logs.ts b/src/middlewares/logs.ts index f5efaa5..a1e0070 100644 --- a/src/middlewares/logs.ts +++ b/src/middlewares/logs.ts @@ -50,7 +50,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { try { rootId = token - ? await new permission().checkOrg(token, req.app.locals.logData.userId) + ? await new permission().checkRootDna(token, req.app.locals.logData.userId) : null; } catch (err) { console.warn("Error fetching rootId:", err); @@ -59,7 +59,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { const obj = { logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info", ip: req.ip, - rootId: rootId?.orgRootId ?? null, + rootId: rootId?.rootDnaId ?? null, systemName: "salary", startTimeStamp: timestamp, endTimeStamp: new Date().toISOString(),