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 = {