diff --git a/docs/migrations/fix_GetProfileEmployeeSalaryLevel_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileEmployeeSalaryLevel_calendar_arithmetic.sql new file mode 100644 index 00000000..bca30538 --- /dev/null +++ b/docs/migrations/fix_GetProfileEmployeeSalaryLevel_calendar_arithmetic.sql @@ -0,0 +1,140 @@ +-- ==================================================================== +-- Fix GetProfileEmployeeSalaryLevel to use calendar arithmetic +-- This changes from fixed formulas to actual calendar arithmetic, +-- matching calculateGovAge and GetProfileSalaryLevel behavior +-- ==================================================================== + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS `GetProfileEmployeeSalaryLevel`$$ + +CREATE DEFINER=`root`@`%` PROCEDURE `GetProfileEmployeeSalaryLevel`( + IN personId VARCHAR(36), + IN _date DATE +) +BEGIN +WITH ordered AS ( + SELECT * + FROM profileSalary + WHERE profileEmployeeId = personId + AND commandCode IN ('0','1','2','3','4','8','9','10','11','12','13','14','15','16','20') +), +work_session AS ( + SELECT *, + COALESCE( + SUM(CASE WHEN commandCode IN (12,15,16) THEN 1 ELSE 0 END) + OVER (ORDER BY commandDateAffect, commandDateSign + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), + 0) AS sessionId + FROM ordered +), +session_end AS ( + SELECT sessionId, MAX(commandDateAffect) AS sessionEndDate + FROM work_session + GROUP BY sessionId +), +level_change AS ( + SELECT *, + CASE + WHEN LAG(positionCee) OVER (ORDER BY commandDateAffect, commandDateSign) <=> positionCee + AND LAG(positionType) OVER (ORDER BY commandDateAffect, commandDateSign) <=> positionType + AND LAG(positionLevel) OVER (ORDER BY commandDateAffect, commandDateSign) <=> positionLevel + AND LAG(sessionId) OVER (ORDER BY commandDateAffect, commandDateSign) = sessionId + THEN 0 + ELSE 1 + END AS isNewLevel + FROM work_session +), +level_group AS ( + SELECT *, + SUM(isNewLevel) OVER (ORDER BY commandDateAffect, commandDateSign) AS levelGroup + FROM level_change +), +first_rows AS ( + SELECT * FROM ( + SELECT *, + ROW_NUMBER() OVER (PARTITION BY levelGroup ORDER BY commandDateAffect, commandDateSign) AS rnLevel + FROM level_group + ) t WHERE rnLevel = 1 +), +rows_with_duration AS ( + SELECT + fr.*, + CASE + WHEN LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) IS NULL + THEN NULL + WHEN LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) <> fr.sessionId + THEN TIMESTAMPDIFF(DAY, fr.commandDateAffect, se.sessionEndDate) + 1 + ELSE + TIMESTAMPDIFF(DAY, fr.commandDateAffect, + LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign)) + END AS duration_days + FROM first_rows fr + LEFT JOIN session_end se ON se.sessionId = fr.sessionId +), +resultWithDiff AS ( + SELECT + *, + LAG(duration_days) OVER (ORDER BY commandDateAffect, commandDateSign) AS days_diff + FROM rows_with_duration +) +SELECT + r.commandDateAffect, + r.positionType, + r.positionLevel, + r.positionCee, + r.days_diff, + CASE + WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN + TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) + ELSE 0 + END AS Years, + CASE + WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN + TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12 + ELSE 0 + END AS Months, + CASE + WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN + DATEDIFF(r.commandDateAffect, + DATE_ADD( + DATE_ADD(LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), + INTERVAL TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) YEAR), + INTERVAL TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12 MONTH) + ) + ELSE 0 + END AS Days, + r.posNo, + r.positionExecutive, + r.orgRoot, + r.orgChild1, + r.orgChild2, + r.orgChild3, + r.orgChild4, + r.commandCode, + r.commandName, + r.commandNo, + r.commandYear, + r.remark +FROM resultWithDiff r + +UNION ALL + +SELECT + _date, NULL, NULL, NULL, + TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1, + TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date), + TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12, + DATEDIFF(_date, + DATE_ADD( + DATE_ADD(MAX(commandDateAffect), + INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR), + INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH) + ), + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL +FROM resultWithDiff; + +END$$ + +DELIMITER ; diff --git a/docs/migrations/fix_GetProfileEmployeeSalaryPosition_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileEmployeeSalaryPosition_calendar_arithmetic.sql new file mode 100644 index 00000000..fa53b467 --- /dev/null +++ b/docs/migrations/fix_GetProfileEmployeeSalaryPosition_calendar_arithmetic.sql @@ -0,0 +1,137 @@ +-- ==================================================================== +-- Fix GetProfileEmployeeSalaryPosition to use calendar arithmetic +-- This changes from fixed formulas to actual calendar arithmetic, +-- matching calculateGovAge and GetProfileSalaryPosition behavior +-- ==================================================================== + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS `GetProfileEmployeeSalaryPosition`$$ + +CREATE DEFINER=`root`@`%` PROCEDURE `GetProfileEmployeeSalaryPosition`( + IN personId VARCHAR(36), + IN _date DATE +) +BEGIN +WITH ordered AS ( + SELECT * FROM profileSalary WHERE profileEmployeeId = personId AND commandCode IN ('0','1','2','3','4','8','9','10','11','12','13','14','15','16','20') +), +work_session AS ( + SELECT *, + COALESCE( + SUM(CASE WHEN commandCode IN (12,15,16) THEN 1 ELSE 0 END) + OVER (ORDER BY commandDateAffect, commandDateSign + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), + 0) AS sessionId + FROM ordered +), +session_end AS ( + SELECT sessionId, MAX(commandDateAffect) AS sessionEndDate + FROM work_session + GROUP BY sessionId +), +position_change AS ( + SELECT *, + CASE + WHEN LAG(positionName) OVER (ORDER BY commandDateAffect, commandDateSign) = positionName + AND LAG(sessionId) OVER (ORDER BY commandDateAffect, commandDateSign) = sessionId + THEN 0 + ELSE 1 + END AS isNewPosition + FROM work_session +), +position_group AS ( + SELECT *, + SUM(isNewPosition) OVER (ORDER BY commandDateAffect, commandDateSign) AS posGroup + FROM position_change +), +first_rows AS ( + SELECT * FROM ( + SELECT *, + ROW_NUMBER() OVER (PARTITION BY posGroup ORDER BY commandDateAffect, commandDateSign) AS rnPos + FROM position_group + ) t WHERE rnPos = 1 +), +rows_with_duration AS ( + SELECT + fr.*, + LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) AS nextSessionId, + CASE + WHEN LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) IS NULL + THEN NULL + WHEN LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) <> fr.sessionId + THEN TIMESTAMPDIFF(DAY, fr.commandDateAffect, se.sessionEndDate) + 1 + ELSE + TIMESTAMPDIFF(DAY, fr.commandDateAffect, + LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign)) + END AS duration_days + FROM first_rows fr + LEFT JOIN session_end se ON se.sessionId = fr.sessionId +), +resultWithDiff AS ( + SELECT + *, + LAG(duration_days) OVER (ORDER BY commandDateAffect, commandDateSign) AS days_diff + FROM rows_with_duration +) +SELECT + r.commandDateAffect, + r.positionName, + r.positionCee, + r.days_diff, + CASE + WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN + TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) + ELSE 0 + END AS Years, + CASE + WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN + TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12 + ELSE 0 + END AS Months, + CASE + WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN + TIMESTAMPDIFF(DAY, + DATE_ADD( + DATE_ADD(LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), + INTERVAL TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) YEAR), + INTERVAL TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12 MONTH), + r.commandDateAffect) + ELSE 0 + END AS Days, + r.posNo, + r.positionExecutive, + r.positionType, + r.positionLevel, + r.orgRoot, + r.orgChild1, + r.orgChild2, + r.orgChild3, + r.orgChild4, + r.commandCode, + r.commandName, + r.commandNo, + r.commandYear, + r.remark +FROM resultWithDiff r + +UNION ALL + +SELECT + _date, NULL, NULL, + TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1, + TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date), + TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12, + DATEDIFF(_date, + DATE_ADD( + DATE_ADD(MAX(commandDateAffect), + INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR), + INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH) + ), + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL +FROM resultWithDiff; + +END$$ + +DELIMITER ; diff --git a/docs/migrations/fix_GetProfileSalaryExecutive_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileSalaryExecutive_calendar_arithmetic.sql index e3585c9f..07d85ee8 100644 --- a/docs/migrations/fix_GetProfileSalaryExecutive_calendar_arithmetic.sql +++ b/docs/migrations/fix_GetProfileSalaryExecutive_calendar_arithmetic.sql @@ -90,12 +90,12 @@ SELECT END AS Months, CASE WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN - TIMESTAMPDIFF(DAY, + DATEDIFF(r.commandDateAffect, DATE_ADD( DATE_ADD(LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), INTERVAL TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) YEAR), - INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) MONTH), - r.commandDateAffect) + 1 + INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12 MONTH) + ) ELSE 0 END AS Days, r.posNo, @@ -121,12 +121,12 @@ SELECT TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1, TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date), TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12, - TIMESTAMPDIFF(DAY, + DATEDIFF(_date, DATE_ADD( DATE_ADD(MAX(commandDateAffect), INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR), - INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) MONTH), - _date) + 1, + INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH) + ), NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, NULL,NULL,NULL,NULL,NULL,NULL FROM resultWithDiff; diff --git a/docs/migrations/fix_GetProfileSalaryLevel_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileSalaryLevel_calendar_arithmetic.sql index ca811a23..0ce8bbb5 100644 --- a/docs/migrations/fix_GetProfileSalaryLevel_calendar_arithmetic.sql +++ b/docs/migrations/fix_GetProfileSalaryLevel_calendar_arithmetic.sql @@ -94,12 +94,12 @@ SELECT END AS Months, CASE WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN - TIMESTAMPDIFF(DAY, + DATEDIFF(r.commandDateAffect, DATE_ADD( DATE_ADD(LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), INTERVAL TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) YEAR), - INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) MONTH), - r.commandDateAffect) + 1 + INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12 MONTH) + ) ELSE 0 END AS Days, r.posNo, @@ -123,12 +123,12 @@ SELECT TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1, TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date), TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12, - TIMESTAMPDIFF(DAY, + DATEDIFF(_date, DATE_ADD( DATE_ADD(MAX(commandDateAffect), INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR), - INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) MONTH), - _date) + 1, + INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH) + ), NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, NULL,NULL,NULL,NULL FROM resultWithDiff; diff --git a/docs/migrations/fix_GetProfileSalaryPosition_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileSalaryPosition_calendar_arithmetic.sql index a546ad97..aed2e9e7 100644 --- a/docs/migrations/fix_GetProfileSalaryPosition_calendar_arithmetic.sql +++ b/docs/migrations/fix_GetProfileSalaryPosition_calendar_arithmetic.sql @@ -96,8 +96,8 @@ SELECT DATE_ADD( DATE_ADD(LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), INTERVAL TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) YEAR), - INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) MONTH), - r.commandDateAffect) + 1 + INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12 MONTH), + r.commandDateAffect) ELSE 0 END AS Days, r.posNo, @@ -124,12 +124,12 @@ SELECT TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1, TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date), TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12, - TIMESTAMPDIFF(DAY, + DATEDIFF(_date, DATE_ADD( DATE_ADD(MAX(commandDateAffect), INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR), - INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) MONTH), - _date) + 1, + INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH) + ), NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, NULL,NULL,NULL,NULL,NULL FROM resultWithDiff; diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index 277ac081..19a9f5e4 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -23,12 +23,21 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; import { In, IsNull, LessThan, MoreThan, Not } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; -import { calculateTenure } from "../utils/tenure"; -import { TenurePositionOfficer } from "../entities/TenurePositionOfficer"; -import { TenureLevelOfficer } from "../entities/TenureLevelOfficer"; -import { TenurePositionEmployee } from "../entities/TenurePositionEmployee"; -import { TenureLevelEmployee } from "../entities/TenureLevelEmployee"; -import { TenurePositionExecutiveOfficer } from "../entities/TenurePositionExecutiveOfficer"; +import { normalizeDurationSumSimple } from "../utils/tenure"; +import { + TenurePositionOfficer, + CreateTenurePositionOfficer, +} from "../entities/TenurePositionOfficer"; +import { TenureLevelOfficer, CreateTenureLevelOfficer } from "../entities/TenureLevelOfficer"; +import { + TenurePositionEmployee, + CreateTenurePositionEmployee, +} from "../entities/TenurePositionEmployee"; +import { TenureLevelEmployee, CreateTenureLevelEmployee } from "../entities/TenureLevelEmployee"; +import { + TenurePositionExecutiveOfficer, + CreateTenurePositionExecutiveOfficer, +} from "../entities/TenurePositionExecutiveOfficer"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; import { OrgRevision } from "../entities/OrgRevision"; @@ -46,138 +55,303 @@ export class ProfileSalaryController extends Controller { private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); private salaryRepo = AppDataSource.getRepository(ProfileSalary); private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); - private positionOfficerRepo = AppDataSource.getRepository(TenurePositionOfficer); - private positionEmployeeRepo = AppDataSource.getRepository(TenurePositionEmployee); - private levelOfficerRepo = AppDataSource.getRepository(TenureLevelOfficer); - private levelEmployeeRepo = AppDataSource.getRepository(TenureLevelEmployee); - private positionExecutiveOfficerRepo = AppDataSource.getRepository( - TenurePositionExecutiveOfficer, - ); private commandRepository = AppDataSource.getRepository(Command); private orgRootRepository = AppDataSource.getRepository(OrgRoot); - private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); - private positionRepo = AppDataSource.getRepository(Position); private registryRepo = AppDataSource.getRepository(Registry); private registryEmployeeRepo = AppDataSource.getRepository(RegistryEmployee); @Get("TenurePositionOfficer") public async cronjobTenurePositionOfficer() { - let data: any = []; - await this.positionOfficerRepo.clear(); - const profile = await this.profileRepo.find(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); const baseCurrentDate = CURRENT_DATE[0].today; - for await (const x of profile) { - // Use leave date if available and valid, otherwise use current date - let _currentDate = baseCurrentDate; - if (x.isLeave && x.leaveDate) { - _currentDate = Extension.toDateOnlyString(x.leaveDate); + + const profiles = await this.profileRepo.find({ + select: ["id", "position", "isLeave", "leaveDate"], + where: { position: Not(IsNull()) }, + }); + + const BATCH_SIZE = 100; + let successCount = 0; + let failCount = 0; + const allData: CreateTenurePositionOfficer[] = []; + + for (let i = 0; i < profiles.length; i += BATCH_SIZE) { + const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); + const results = await Promise.allSettled( + batch.map((profile) => + this.processSingleProfileForTenurePositionOfficer(profile, baseCurrentDate), + ), + ); + + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + allData.push(result.value); + successCount++; + } else { + failCount++; + } + }); + } + + await AppDataSource.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.delete(TenurePositionOfficer, {}); + if (allData.length > 0) { + const entities = allData.map((data) => { + const entity = new TenurePositionOfficer(); + Object.assign(entity, data); + return entity; + }); + await transactionalEntityManager.save(TenurePositionOfficer, entities); } + }); + + return new HttpSuccess({ + message: `อัปเดต tenure position officer สำเร็จ`, + total: profiles.length, + success: successCount, + failed: failCount, + }); + } + + private async processSingleProfileForTenurePositionOfficer( + profile: Pick, + baseCurrentDate: string, + ): Promise { + try { + let _currentDate = baseCurrentDate; + if (profile.isLeave && profile.leaveDate) { + _currentDate = Extension.toDateOnlyString(profile.leaveDate); + } + const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [ - x.id, + profile.id, _currentDate, ]); const _position = position.length > 0 ? position[0] : []; + const mapPosition = _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ - days_diff: curr.days_diff, positionName: _position[index]?.positionName, + year: + curr.Years !== null && curr.Years !== undefined + ? Math.floor(Number(curr.Years)) + : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: + curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - const calDayDiff = mapPosition - .filter((curr: any) => curr.positionName == x.position) - .reduce( - (acc: any, curr: any) => { - acc.days_diff += Number(curr.days_diff) || 0; - acc.positionName = curr.positionName; - return acc; - }, - { days_diff: 0, positionName: null }, - ); - const { year, month, day } = calculateTenure(calDayDiff.days_diff); - const mapData: any = { - profileId: x.id, - positionName: calDayDiff.positionName, - days_diff: calDayDiff.days_diff, - Years: year, - Months: month, - Days: day, - }; - data.push(mapData); - } - await this.positionOfficerRepo.save(data); - return new HttpSuccess(); + const currentTenure = mapPosition.find((curr: any) => curr.positionName === profile.position); + + if (currentTenure) { + const normalized = normalizeDurationSumSimple( + currentTenure.year, + currentTenure.month, + currentTenure.day, + ); + return { + profileId: profile.id, + positionName: currentTenure.positionName, + days_diff: null, + Years: normalized.years, + Months: normalized.months, + Days: normalized.days, + }; + } + return null; + } catch (error) { + return null; + } } @Get("TenurePositionEmployee") public async cronjobTenurePositionEmployee() { - let data: any = []; - await this.positionEmployeeRepo.clear(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); const baseCurrentDate = CURRENT_DATE[0].today; - const profile = await this.profileEmployeeRepo.find(); - for await (const x of profile) { - // Use leave date if available and valid, otherwise use current date - let _currentDate = baseCurrentDate; - if (x?.isLeave && x.leaveDate) { - _currentDate = Extension.toDateOnlyString(x.leaveDate); + + const profiles = await this.profileEmployeeRepo.find({ + select: ["id", "position", "isLeave", "leaveDate"], + where: { position: Not(IsNull()) }, + }); + + const BATCH_SIZE = 100; + let successCount = 0; + let failCount = 0; + const allData: CreateTenurePositionEmployee[] = []; + + for (let i = 0; i < profiles.length; i += BATCH_SIZE) { + const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); + const results = await Promise.allSettled( + batch.map((profile) => + this.processSingleProfileForTenurePositionEmployee(profile, baseCurrentDate), + ), + ); + + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + allData.push(result.value); + successCount++; + } else { + failCount++; + } + }); + } + + await AppDataSource.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.delete(TenurePositionEmployee, {}); + if (allData.length > 0) { + const entities = allData.map((data) => { + const entity = new TenurePositionEmployee(); + Object.assign(entity, data); + return entity; + }); + await transactionalEntityManager.save(TenurePositionEmployee, entities); } + }); + + return new HttpSuccess({ + message: `อัปเดต tenure position employee สำเร็จ`, + total: profiles.length, + success: successCount, + failed: failCount, + }); + } + + private async processSingleProfileForTenurePositionEmployee( + profile: Pick, + baseCurrentDate: string, + ): Promise { + try { + let _currentDate = baseCurrentDate; + if (profile.isLeave && profile.leaveDate) { + _currentDate = Extension.toDateOnlyString(profile.leaveDate); + } + const position = await AppDataSource.query("CALL GetProfileEmployeeSalaryPosition(?, ?)", [ - x.id, + profile.id, _currentDate, ]); const _position = position.length > 0 ? position[0] : []; + const mapPosition = _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ - days_diff: curr.days_diff, positionName: _position[index]?.positionName, + year: + curr.Years !== null && curr.Years !== undefined + ? Math.floor(Number(curr.Years)) + : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: + curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - const calDayDiff = mapPosition - .filter((curr: any) => curr.positionName == x.position) - .reduce( - (acc: any, curr: any) => { - acc.days_diff += Number(curr.days_diff) || 0; - acc.positionName = curr.positionName; - return acc; - }, - { days_diff: 0, positionName: null }, - ); - const { year, month, day } = calculateTenure(calDayDiff.days_diff); - const mapData: any = { - profileEmployeeId: x.id, - positionName: calDayDiff.positionName, - days_diff: calDayDiff.days_diff, - Years: year, - Months: month, - Days: day, - }; - data.push(mapData); - } - await this.positionEmployeeRepo.save(data); - return new HttpSuccess(); + const currentTenure = mapPosition.find((curr: any) => curr.positionName === profile.position); + + if (currentTenure) { + const normalized = normalizeDurationSumSimple( + currentTenure.year, + currentTenure.month, + currentTenure.day, + ); + return { + profileEmployeeId: profile.id, + positionName: currentTenure.positionName, + days_diff: null, + Years: normalized.years, + Months: normalized.months, + Days: normalized.days, + }; + } + return null; + } catch (error) { + return null; + } } @Get("TenureLevelOfficer") public async cronjobTenureLevelOfficer() { - let data: any = []; - await this.levelOfficerRepo.clear(); - const profile = await this.profileRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); const baseCurrentDate = CURRENT_DATE[0].today; - for await (const x of profile) { - // Use leave date if available and valid, otherwise use current date - let _currentDate = baseCurrentDate; - if (x?.isLeave && x.leaveDate) { - _currentDate = Extension.toDateOnlyString(x.leaveDate); + + const profiles = await this.profileRepo.find({ + relations: ["posLevel", "posType"], + select: ["id", "isLeave", "leaveDate", "posLevel", "posType"], + where: { + posLevel: Not(IsNull()), + posType: Not(IsNull()), + }, + }); + + const BATCH_SIZE = 100; + let successCount = 0; + let failCount = 0; + const allData: CreateTenureLevelOfficer[] = []; + + for (let i = 0; i < profiles.length; i += BATCH_SIZE) { + const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); + const results = await Promise.allSettled( + batch.map((profile) => + this.processSingleProfileForTenureLevelOfficer(profile, baseCurrentDate), + ), + ); + + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + allData.push(result.value); + successCount++; + } else { + failCount++; + } + }); + } + + await AppDataSource.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.delete(TenureLevelOfficer, {}); + if (allData.length > 0) { + const entities = allData.map((data) => { + const entity = new TenureLevelOfficer(); + Object.assign(entity, data); + return entity; + }); + await transactionalEntityManager.save(TenureLevelOfficer, entities); } + }); + + return new HttpSuccess({ + message: `อัปเดต tenure level officer สำเร็จ`, + total: profiles.length, + success: successCount, + failed: failCount, + }); + } + + private async processSingleProfileForTenureLevelOfficer( + profile: Pick & { + posLevel?: { posLevelName?: string } | null; + posType?: { posTypeName?: string } | null; + }, + baseCurrentDate: string, + ): Promise { + try { + let _currentDate = baseCurrentDate; + if (profile.isLeave && profile.leaveDate) { + _currentDate = Extension.toDateOnlyString(profile.leaveDate); + } + const positionLevel = await AppDataSource.query("CALL GetProfileSalaryLevel(?, ?)", [ - x.id, + profile.id, _currentDate, ]); const _positionLevel = positionLevel.length > 0 ? positionLevel[0] : []; + const mapPositionLevel = _positionLevel.length > 1 ? _positionLevel.slice(1).map((curr: any, index: number) => ({ @@ -185,13 +359,24 @@ export class ProfileSalaryController extends Controller { positionType: _positionLevel[index]?.positionType, positionLevel: _positionLevel[index]?.positionLevel, positionCee: _positionLevel[index]?.positionCee, + year: + curr.Years !== null && curr.Years !== undefined + ? Math.floor(Number(curr.Years)) + : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: + curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; + const calDayDiff = mapPositionLevel .filter( (curr: any) => - curr.positionLevel == (x.posLevel?.posLevelName ?? null) && - curr.positionType == (x.posType?.posTypeName ?? null), + curr.positionLevel === (profile.posLevel?.posLevelName ?? null) && + curr.positionType === (profile.posType?.posTypeName ?? null), ) .reduce( (acc: any, curr: any) => { @@ -199,45 +384,118 @@ export class ProfileSalaryController extends Controller { acc.positionType = curr.positionType; acc.positionLevel = curr.positionLevel; acc.positionCee = curr.positionCee; + acc.year += curr.year; + acc.month += curr.month; + acc.day += curr.day; return acc; }, - { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, + { + days_diff: 0, + positionType: null, + positionLevel: null, + positionCee: null, + year: 0, + month: 0, + day: 0, + }, ); - const { year, month, day } = calculateTenure(calDayDiff.days_diff); - const mapData: any = { - profileId: x.id, + + const normalized = normalizeDurationSumSimple( + calDayDiff.year, + calDayDiff.month, + calDayDiff.day, + ); + + return { + profileId: profile.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: x.posLevel == null ? 0 : year.toFixed(4), - Months: x.posLevel == null ? 0 : month.toFixed(4), - Days: x.posLevel == null ? 0 : day.toFixed(4), + Years: profile.posLevel == null ? 0 : normalized.years, + Months: profile.posLevel == null ? 0 : normalized.months, + Days: profile.posLevel == null ? 0 : normalized.days, }; - data.push(mapData); + } catch (error) { + return null; } - await this.levelOfficerRepo.save(data); - - return new HttpSuccess(); } @Get("TenureLevelEmployee") public async cronjobTenureLevelEmployee() { - let data: any = []; - await this.levelEmployeeRepo.clear(); - const profile = await this.profileEmployeeRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); const baseCurrentDate = CURRENT_DATE[0].today; - for await (const x of profile) { - // Use leave date if available and valid, otherwise use current date - let _currentDate = baseCurrentDate; - if (x?.isLeave && x.leaveDate) { - _currentDate = Extension.toDateOnlyString(x.leaveDate); + + const profiles = await this.profileEmployeeRepo.find({ + relations: ["posLevel", "posType"], + select: ["id", "isLeave", "leaveDate", "posLevel", "posType"], + where: { + posLevel: Not(IsNull()), + posType: Not(IsNull()), + }, + }); + + const BATCH_SIZE = 100; + let successCount = 0; + let failCount = 0; + const allData: CreateTenureLevelEmployee[] = []; + + for (let i = 0; i < profiles.length; i += BATCH_SIZE) { + const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); + const results = await Promise.allSettled( + batch.map((profile) => + this.processSingleProfileForTenureLevelEmployee(profile, baseCurrentDate), + ), + ); + + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + allData.push(result.value); + successCount++; + } else { + failCount++; + } + }); + } + + await AppDataSource.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.delete(TenureLevelEmployee, {}); + if (allData.length > 0) { + const entities = allData.map((data) => { + const entity = new TenureLevelEmployee(); + Object.assign(entity, data); + return entity; + }); + await transactionalEntityManager.save(TenureLevelEmployee, entities); } + }); + + return new HttpSuccess({ + message: `อัปเดต tenure level employee สำเร็จ`, + total: profiles.length, + success: successCount, + failed: failCount, + }); + } + + private async processSingleProfileForTenureLevelEmployee( + profile: Pick & { + posLevel?: { posLevelName?: string } | null; + posType?: { posTypeName?: string } | null; + }, + baseCurrentDate: string, + ): Promise { + try { + let _currentDate = baseCurrentDate; + if (profile.isLeave && profile.leaveDate) { + _currentDate = Extension.toDateOnlyString(profile.leaveDate); + } + const positionLevel = await AppDataSource.query("CALL GetProfileEmployeeSalaryLevel(?, ?)", [ - x.id, + profile.id, _currentDate, ]); const _positionLevel = positionLevel.length > 0 ? positionLevel[0] : []; + const mapPositionLevel = _positionLevel.length > 1 ? _positionLevel.slice(1).map((curr: any, index: number) => ({ @@ -245,13 +503,24 @@ export class ProfileSalaryController extends Controller { positionType: _positionLevel[index]?.positionType, positionLevel: _positionLevel[index]?.positionLevel, positionCee: _positionLevel[index]?.positionCee, + year: + curr.Years !== null && curr.Years !== undefined + ? Math.floor(Number(curr.Years)) + : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: + curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; + const calDayDiff = mapPositionLevel .filter( (curr: any) => - curr.positionLevel == (x.posLevel?.posLevelName ?? null) && - curr.positionType == (x.posType?.posTypeName ?? null), + curr.positionLevel === (profile.posLevel?.posLevelName ?? null) && + curr.positionType === (profile.posType?.posTypeName ?? null), ) .reduce( (acc: any, curr: any) => { @@ -259,97 +528,161 @@ export class ProfileSalaryController extends Controller { acc.positionType = curr.positionType; acc.positionLevel = curr.positionLevel; acc.positionCee = curr.positionCee; + acc.year += curr.year; + acc.month += curr.month; + acc.day += curr.day; return acc; }, - { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, + { + days_diff: 0, + positionType: null, + positionLevel: null, + positionCee: null, + year: 0, + month: 0, + day: 0, + }, ); - const { year, month, day } = calculateTenure(calDayDiff.days_diff); - const mapData: any = { - profileEmployeeId: x.id, + + const normalized = normalizeDurationSumSimple( + calDayDiff.year, + calDayDiff.month, + calDayDiff.day, + ); + + return { + profileEmployeeId: profile.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: x.posLevel == null ? 0 : year.toFixed(4), - Months: x.posLevel == null ? 0 : month.toFixed(4), - Days: x.posLevel == null ? 0 : day.toFixed(4), + Years: profile.posLevel == null ? 0 : normalized.years, + Months: profile.posLevel == null ? 0 : normalized.months, + Days: profile.posLevel == null ? 0 : normalized.days, }; - data.push(mapData); + } catch (error) { + return null; } - await this.levelEmployeeRepo.save(data); - - return new HttpSuccess(); } @Get("TenurePositionExecutiveOfficer") public async cronjobTenureExecutivePositionOfficer() { - let data: any = []; - await this.positionExecutiveOfficerRepo.clear(); - const profile = await this.profileRepo.find(); - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); const baseCurrentDate = CURRENT_DATE[0].today; - for await (const x of profile) { - // Use leave date if available and valid, otherwise use current date - let _currentDate = baseCurrentDate; - if (x?.isLeave && x.leaveDate) { - _currentDate = Extension.toDateOnlyString(x.leaveDate); - } - const position = await this.positionRepo.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: orgRevision?.id, - current_holderId: x.id, - }, - }, - order: { createdAt: "DESC" }, - relations: { - posExecutive: true, - }, + + const profiles = await this.profileRepo.find({ + select: ["id", "posExecutive", "isLeave", "leaveDate"], + where: { posExecutive: Not(IsNull()) }, + }); + + const BATCH_SIZE = 100; + let successCount = 0; + let failCount = 0; + const allData: CreateTenurePositionExecutiveOfficer[] = []; + + for (let i = 0; i < profiles.length; i += BATCH_SIZE) { + const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); + const results = await Promise.allSettled( + batch.map((profile) => + this.processSingleProfileForTenureExecutivePositionOfficer(profile, baseCurrentDate), + ), + ); + + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + allData.push(result.value); + successCount++; + } else { + failCount++; + } }); + } + + await AppDataSource.transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.delete(TenurePositionExecutiveOfficer, {}); + if (allData.length > 0) { + const entities = allData.map((data) => { + const entity = new TenurePositionExecutiveOfficer(); + Object.assign(entity, data); + return entity; + }); + await transactionalEntityManager.save(TenurePositionExecutiveOfficer, entities); + } + }); + + return new HttpSuccess({ + message: `อัปเดต tenure executive position officer สำเร็จ`, + total: profiles.length, + success: successCount, + failed: failCount, + }); + } + + private async processSingleProfileForTenureExecutivePositionOfficer( + profile: Pick, + baseCurrentDate: string, + ): Promise { + try { + let _currentDate = baseCurrentDate; + if (profile.isLeave && profile.leaveDate) { + _currentDate = Extension.toDateOnlyString(profile.leaveDate); + } + const positionExecutive = await AppDataSource.query("CALL GetProfileSalaryExecutive(?, ?)", [ - x.id, + profile.id, _currentDate, ]); const _position = positionExecutive.length > 0 ? positionExecutive[0] : []; + const mapPosition = _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days_diff: curr.days_diff, positionExecutive: _position[index]?.positionExecutive, + year: + curr.Years !== null && curr.Years !== undefined + ? Math.floor(Number(curr.Years)) + : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: + curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - const _posExecutiveName = position?.posExecutive?.posExecutiveName; + const calDayDiff = mapPosition - .filter((curr: any) => _posExecutiveName && curr.positionExecutive == _posExecutiveName) + .filter((curr: any) => curr.positionExecutive === profile.posExecutive) .reduce( (acc: any, curr: any) => { acc.days_diff += Number(curr.days_diff) || 0; acc.positionExecutive = curr.positionExecutive; + acc.year += curr.year; + acc.month += curr.month; + acc.day += curr.day; return acc; }, - { days_diff: 0, positionExecutive: null }, + { days_diff: 0, positionExecutive: null, year: 0, month: 0, day: 0 }, ); - const { year, month, day } = calculateTenure(calDayDiff.days_diff); - const mapData: any = { - profileId: x.id, + + const normalized = normalizeDurationSumSimple( + calDayDiff.year, + calDayDiff.month, + calDayDiff.day, + ); + + return { + profileId: profile.id, positionExecutiveName: calDayDiff.positionExecutive, days_diff: calDayDiff.days_diff, - Years: year.toFixed(4), - Months: month.toFixed(4), - Days: day.toFixed(4), + Years: normalized.years, + Months: normalized.months, + Days: normalized.days, }; - data.push(mapData); + } catch (error) { + return null; } - await this.positionExecutiveOfficerRepo.save(data); - return new HttpSuccess(); } @Get("Registry") @@ -596,8 +929,12 @@ export class ProfileSalaryController extends Controller { ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, // Use stored procedure's calculated values (calendar arithmetic) - year: curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: curr.Months !== null && curr.Months !== undefined ? Math.floor(Number(curr.Months)) : 0, + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) @@ -613,19 +950,21 @@ export class ProfileSalaryController extends Controller { existing.month += curr.month; existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days, year: curr.year, month: curr.month, day: curr.day }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - // Normalize the summed values (convert excess days to months, months to years) - while (existing.day >= 30) { - existing.month += Math.floor(existing.day / 30); - existing.day = existing.day % 30; - } - while (existing.month >= 12) { - existing.year += Math.floor(existing.month / 12); - existing.month = existing.month % 12; - } + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -641,8 +980,12 @@ export class ProfileSalaryController extends Controller { ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, // Use stored procedure's calculated values (calendar arithmetic) - year: curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: curr.Months !== null && curr.Months !== undefined ? Math.floor(Number(curr.Months)) : 0, + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee @@ -664,19 +1007,21 @@ export class ProfileSalaryController extends Controller { existing.month += curr.month; existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days, year: curr.year, month: curr.month, day: curr.day }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - // Normalize the summed values (convert excess days to months, months to years) - while (existing.day >= 30) { - existing.month += Math.floor(existing.day / 30); - existing.day = existing.day % 30; - } - while (existing.month >= 12) { - existing.year += Math.floor(existing.month / 12); - existing.month = existing.month % 12; - } + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -693,8 +1038,12 @@ export class ProfileSalaryController extends Controller { ? _posExecutive.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, // Use stored procedure's calculated values (calendar arithmetic) - year: curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: curr.Months !== null && curr.Months !== undefined ? Math.floor(Number(curr.Months)) : 0, + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _posExecutive[index]?.positionExecutive, })) @@ -710,19 +1059,21 @@ export class ProfileSalaryController extends Controller { existing.month += curr.month; existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days, year: curr.year, month: curr.month, day: curr.day }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - // Normalize the summed values (convert excess days to months, months to years) - while (existing.day >= 30) { - existing.month += Math.floor(existing.day / 30); - existing.day = existing.day % 30; - } - while (existing.month >= 12) { - existing.year += Math.floor(existing.month / 12); - existing.month = existing.month % 12; - } + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -766,8 +1117,12 @@ export class ProfileSalaryController extends Controller { ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, // Use stored procedure's calculated values (calendar arithmetic) - year: curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: curr.Months !== null && curr.Months !== undefined ? Math.floor(Number(curr.Months)) : 0, + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) @@ -783,19 +1138,21 @@ export class ProfileSalaryController extends Controller { existing.month += curr.month; existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days, year: curr.year, month: curr.month, day: curr.day }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - // Normalize the summed values (convert excess days to months, months to years) - while (existing.day >= 30) { - existing.month += Math.floor(existing.day / 30); - existing.day = existing.day % 30; - } - while (existing.month >= 12) { - existing.year += Math.floor(existing.month / 12); - existing.month = existing.month % 12; - } + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -812,8 +1169,12 @@ export class ProfileSalaryController extends Controller { ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, // Use stored procedure's calculated values (calendar arithmetic) - year: curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: curr.Months !== null && curr.Months !== undefined ? Math.floor(Number(curr.Months)) : 0, + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee @@ -835,19 +1196,21 @@ export class ProfileSalaryController extends Controller { existing.month += curr.month; existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days, year: curr.year, month: curr.month, day: curr.day }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - // Normalize the summed values (convert excess days to months, months to years) - while (existing.day >= 30) { - existing.month += Math.floor(existing.day / 30); - existing.day = existing.day % 30; - } - while (existing.month >= 12) { - existing.year += Math.floor(existing.month / 12); - existing.month = existing.month % 12; - } + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -864,8 +1227,12 @@ export class ProfileSalaryController extends Controller { ? _posExecutive.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, // Use stored procedure's calculated values (calendar arithmetic) - year: curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: curr.Months !== null && curr.Months !== undefined ? Math.floor(Number(curr.Months)) : 0, + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _posExecutive[index]?.positionExecutive, })) @@ -881,19 +1248,21 @@ export class ProfileSalaryController extends Controller { existing.month += curr.month; existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days, year: curr.year, month: curr.month, day: curr.day }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - // Normalize the summed values (convert excess days to months, months to years) - while (existing.day >= 30) { - existing.month += Math.floor(existing.day / 30); - existing.day = existing.day % 30; - } - while (existing.month >= 12) { - existing.year += Math.floor(existing.month / 12); - existing.month = existing.month % 12; - } + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, diff --git a/src/controllers/ProfileSalaryEmployeeController.ts b/src/controllers/ProfileSalaryEmployeeController.ts index 44b93a5d..5f2ea997 100644 --- a/src/controllers/ProfileSalaryEmployeeController.ts +++ b/src/controllers/ProfileSalaryEmployeeController.ts @@ -27,7 +27,7 @@ import { Profile } from "../entities/Profile"; import { In, LessThan, IsNull, MoreThan } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; -import { calculateTenure } from "../utils/tenure"; +import { normalizeDurationSumSimple } from "../utils/tenure"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; import Extension from "../interfaces/extension"; @@ -161,6 +161,14 @@ export class ProfileSalaryEmployeeController extends Controller { _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, + // Use stored procedure's calculated values (calendar arithmetic) + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) : []; @@ -171,15 +179,25 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; + existing.year += curr.year; + existing.month += curr.month; + existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - const { year, month, day } = calculateTenure(existing.days); - existing.year = year; - existing.month = month; - existing.day = day; + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -195,6 +213,14 @@ export class ProfileSalaryEmployeeController extends Controller { _posLevel.length > 1 ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, + // Use stored procedure's calculated values (calendar arithmetic) + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee ? `ระดับ ${_posLevel[index]?.positionCee.trim()}` @@ -208,15 +234,25 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; + existing.year += curr.year; + existing.month += curr.month; + existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - const { year, month, day } = calculateTenure(existing.days); - existing.year = year; - existing.month = month; - existing.day = day; + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -254,6 +290,14 @@ export class ProfileSalaryEmployeeController extends Controller { _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, + // Use stored procedure's calculated values (calendar arithmetic) + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) : []; @@ -264,15 +308,25 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; + existing.year += curr.year; + existing.month += curr.month; + existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - const { year, month, day } = calculateTenure(existing.days); - existing.year = year; - existing.month = month; - existing.day = day; + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, @@ -288,6 +342,14 @@ export class ProfileSalaryEmployeeController extends Controller { _posLevel.length > 1 ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, + // Use stored procedure's calculated values (calendar arithmetic) + year: + curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, + month: + curr.Months !== null && curr.Months !== undefined + ? Math.floor(Number(curr.Months)) + : 0, + day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee ? `ระดับ ${_posLevel[index]?.positionCee.trim()}` @@ -301,15 +363,25 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; + existing.year += curr.year; + existing.month += curr.month; + existing.day += curr.day; } else { - existing = { name: curr.name, days: curr.days }; + existing = { + name: curr.name, + days: curr.days, + year: curr.year, + month: curr.month, + day: curr.day, + }; acc.push(existing); } - const { year, month, day } = calculateTenure(existing.days); - existing.year = year; - existing.month = month; - existing.day = day; + // Normalize the summed values using calendar arithmetic + const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); + existing.year = normalized.years; + existing.month = normalized.months; + existing.day = normalized.days; return acc; }, diff --git a/src/entities/TenureLevelEmployee.ts b/src/entities/TenureLevelEmployee.ts index 36ae0176..5654e306 100644 --- a/src/entities/TenureLevelEmployee.ts +++ b/src/entities/TenureLevelEmployee.ts @@ -74,7 +74,7 @@ export class TenureLevelEmployee extends EntityBase { positionLevel: string; } -export class CreateTenureLevelOfficer { +export class CreateTenureLevelEmployee { profileEmployeeId: string; positionCee: string | null; days_diff: number | null; diff --git a/src/utils/tenure.ts b/src/utils/tenure.ts index dbdedbb3..1c97dff1 100644 --- a/src/utils/tenure.ts +++ b/src/utils/tenure.ts @@ -1,18 +1,37 @@ /** - * คำนวณอายุงานจากจำนวนวันรวม - * ใช้สูตรเดียวกับ Stored Procedure GetProfileSalaryPosition - * @param totalDays จำนวนวันรวม - * @returns { year, month, day } ปี เดือน วัน + * Normalize a duration sum using calendar arithmetic + * Converts excess days to months using average month length (30.4375 days) + * and excess months to years. Matches the logic used in stored procedures. + * + * @param years Total years from sum + * @param months Total months from sum + * @param days Total days from sum + * @returns Normalized { years, months, days } */ -export function calculateTenure(totalDays: number) { - // Match stored procedure formula: - // days_diff / 365.2524 AS Years - // (days_diff / 30.4375) % 12 AS Months - // days_diff % 30.4375 AS Days +export function normalizeDurationSumSimple( + years: number, + months: number, + days: number, +): { years: number; months: number; days: number } { + const DAYS_PER_MONTH = 30.4375; // Average days per month in Gregorian calendar - const year = Math.floor(totalDays / 365.2524); - const month = Math.floor((totalDays / 30.4375) % 12); - const day = Math.floor(totalDays % 30.4375); + let totalMonths = months; + let totalDays = days; - return { year, month, day }; + // Convert excess days to months + if (totalDays >= DAYS_PER_MONTH) { + const additionalMonths = Math.floor(totalDays / DAYS_PER_MONTH); + totalMonths += additionalMonths; + totalDays = totalDays - additionalMonths * DAYS_PER_MONTH; + } + + // Convert excess months to years + let totalYears = years; + if (totalMonths >= 12) { + const additionalYears = Math.floor(totalMonths / 12); + totalYears += additionalYears; + totalMonths = totalMonths % 12; + } + + return { years: totalYears, months: Math.floor(totalMonths), days: Math.floor(totalDays) }; }