fixed tenure
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m20s

This commit is contained in:
Warunee Tamkoo 2026-05-18 23:25:09 +07:00
parent 5ea111a3c5
commit 6c5356ca46
2 changed files with 379 additions and 127 deletions

View file

@ -24,11 +24,20 @@ import { In, IsNull, LessThan, MoreThan, Not } from "typeorm";
import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils";
import { normalizeDurationSumSimple } 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 {
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,44 +55,84 @@ 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<Profile, "id" | "position" | "isLeave" | "leaveDate">,
baseCurrentDate: string,
): Promise<CreateTenurePositionOfficer | null> {
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] : [];
// Filter for current position and use SP's calculated values (calendar arithmetic)
const mapPosition =
_position.length > 1
? _position.slice(1).map((curr: any, index: number) => ({
positionName: _position[index]?.positionName,
// Use stored procedure's calculated values (calendar arithmetic)
year:
curr.Years !== null && curr.Years !== undefined
? Math.floor(Number(curr.Years))
@ -96,51 +145,102 @@ export class ProfileSalaryController extends Controller {
curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0,
}))
: [];
const currentTenure = mapPosition.find((curr: any) => curr.positionName == x.position);
const currentTenure = mapPosition.find((curr: any) => curr.positionName === profile.position);
if (currentTenure) {
const normalized = normalizeDurationSumSimple(
currentTenure.year,
currentTenure.month,
currentTenure.day,
);
const mapData: any = {
profileId: x.id,
return {
profileId: profile.id,
positionName: currentTenure.positionName,
days_diff: null,
Years: normalized.years,
Months: normalized.months,
Days: normalized.days,
};
data.push(mapData);
}
return null;
} catch (error) {
return null;
}
await this.positionOfficerRepo.save(data);
return new HttpSuccess();
}
@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<ProfileEmployee, "id" | "position" | "isLeave" | "leaveDate">,
baseCurrentDate: string,
): Promise<CreateTenurePositionEmployee | null> {
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] : [];
// Filter for current position and use SP's calculated values (calendar arithmetic)
const mapPosition =
_position.length > 1
? _position.slice(1).map((curr: any, index: number) => ({
positionName: _position[index]?.positionName,
// Use stored procedure's calculated values (calendar arithmetic)
year:
curr.Years !== null && curr.Years !== undefined
? Math.floor(Number(curr.Years))
@ -153,45 +253,105 @@ export class ProfileSalaryController extends Controller {
curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0,
}))
: [];
const currentTenure = mapPosition.find((curr: any) => curr.positionName == x.position);
const currentTenure = mapPosition.find((curr: any) => curr.positionName === profile.position);
if (currentTenure) {
const normalized = normalizeDurationSumSimple(
currentTenure.year,
currentTenure.month,
currentTenure.day,
);
const mapData: any = {
profileEmployeeId: x.id,
return {
profileEmployeeId: profile.id,
positionName: currentTenure.positionName,
days_diff: null,
Years: normalized.years,
Months: normalized.months,
Days: normalized.days,
};
data.push(mapData);
}
return null;
} catch (error) {
return null;
}
await this.positionEmployeeRepo.save(data);
return new HttpSuccess();
}
@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<Profile, "id" | "isLeave" | "leaveDate" | "posLevel" | "posType"> & {
posLevel?: { posLevelName?: string } | null;
posType?: { posTypeName?: string } | null;
},
baseCurrentDate: string,
): Promise<CreateTenureLevelOfficer | null> {
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) => ({
@ -211,11 +371,12 @@ export class ProfileSalaryController extends Controller {
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) => {
@ -238,45 +399,103 @@ export class ProfileSalaryController extends Controller {
day: 0,
},
);
const normalized = normalizeDurationSumSimple(
calDayDiff.year,
calDayDiff.month,
calDayDiff.day,
);
const mapData: any = {
profileId: x.id,
return {
profileId: profile.id,
positionType: calDayDiff.positionType,
positionLevel: calDayDiff.positionLevel,
positionCee: calDayDiff.positionCee,
days_diff: calDayDiff.days_diff,
Years: x.posLevel == null ? 0 : normalized.years,
Months: x.posLevel == null ? 0 : normalized.months,
Days: x.posLevel == null ? 0 : normalized.days,
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<ProfileEmployee, "id" | "isLeave" | "leaveDate" | "posLevel" | "posType"> & {
posLevel?: { posLevelName?: string } | null;
posType?: { posTypeName?: string } | null;
},
baseCurrentDate: string,
): Promise<CreateTenureLevelEmployee | null> {
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) => ({
@ -296,11 +515,12 @@ export class ProfileSalaryController extends Controller {
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) => {
@ -323,66 +543,97 @@ export class ProfileSalaryController extends Controller {
day: 0,
},
);
const normalized = normalizeDurationSumSimple(
calDayDiff.year,
calDayDiff.month,
calDayDiff.day,
);
const mapData: any = {
profileEmployeeId: x.id,
return {
profileEmployeeId: profile.id,
positionType: calDayDiff.positionType,
positionLevel: calDayDiff.positionLevel,
positionCee: calDayDiff.positionCee,
days_diff: calDayDiff.days_diff,
Years: x.posLevel == null ? 0 : normalized.years,
Months: x.posLevel == null ? 0 : normalized.months,
Days: x.posLevel == null ? 0 : normalized.days,
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<Profile, "id" | "isLeave" | "leaveDate" | "posExecutive">,
baseCurrentDate: string,
): Promise<CreateTenurePositionExecutiveOfficer | null> {
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) => ({
@ -400,9 +651,9 @@ export class ProfileSalaryController extends Controller {
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;
@ -414,23 +665,24 @@ export class ProfileSalaryController extends Controller {
},
{ days_diff: 0, positionExecutive: null, year: 0, month: 0, day: 0 },
);
const normalized = normalizeDurationSumSimple(
calDayDiff.year,
calDayDiff.month,
calDayDiff.day,
);
const mapData: any = {
profileId: x.id,
return {
profileId: profile.id,
positionExecutiveName: calDayDiff.positionExecutive,
days_diff: calDayDiff.days_diff,
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")

View file

@ -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;