Compare commits

..

32 commits
develop ... dev

Author SHA1 Message Date
7c6991abe5 fixed isGov
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m14s
2026-04-28 18:07:01 +07:00
5caa7db75a fixed
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
2026-04-28 17:12:54 +07:00
190a5d665a fixed add isGovernment & commandDateAffect
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m12s
2026-04-28 16:53:15 +07:00
2a5fba2dfc fix import temp profile salary add isGovernment & dateGovernment 2026-04-28 16:31:08 +07:00
3163b701c9 reset password change profileId to keycloak
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m12s
2026-04-28 15:50:00 +07:00
harid
58afa49fcd insert profileSalary เดิมเข้ามายัง profile ใหม่ #232
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m8s
2026-04-28 15:17:16 +07:00
d82cd842f6 add reset password by admin & super_admin
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
2026-04-28 15:14:47 +07:00
3833901bea fixed #2436 add link in noti request idp 2026-04-28 14:57:51 +07:00
2417c90dc2 add api sync-missing-emptype
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
2026-04-28 11:38:47 +07:00
b5fb2346ab fixed handle error connect keycloak 2026-04-28 11:05:00 +07:00
071140d98a fixed error update user keycloak data lost
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m17s
2026-04-28 10:03:51 +07:00
28319f443f add api get profile keycloak/position-checkin
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m26s
2026-04-27 19:13:21 +07:00
8705d1abf5 update 2026-04-24 16:15:47 +07:00
2cbc6569e3 update script sql 2026-04-24 13:41:10 +07:00
b9b73ca994 rollback code cronjob time
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m27s
2026-04-24 13:07:26 +07:00
ec6b4a7ac8 fix calculate
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m22s
2026-04-24 12:19:17 +07:00
c348a10207 Merge branch 'develop' into refactor/cronjob-position
* develop:
  fixed#1568 แก้ไขรายการตำแหน่งติดเงื่อนไข
  fix bug
  update path sql script
  #231 และ #2438 checkpoint
2026-04-24 11:39:23 +07:00
b8ef607078 Merge branch 'develop' of github.com:Frappet/bma-ehr-organization into develop
* 'develop' of github.com:Frappet/bma-ehr-organization:
  fix bug
  update path sql script
  #231 และ #2438 checkpoint
2026-04-24 11:38:57 +07:00
5980c140f0 fixed#1568 แก้ไขรายการตำแหน่งติดเงื่อนไข 2026-04-24 11:38:45 +07:00
da4fd18e08 fix bug
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m22s
2026-04-24 11:07:55 +07:00
1d16f78132 update path sql script
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m24s
2026-04-24 10:31:42 +07:00
8f83ab781b fix.save batch insert 2026-04-24 10:28:30 +07:00
d46dd03eaf Merge branch 'develop' into adiDev 2026-04-24 09:26:03 +07:00
harid
8912e83227 api import profileSalaryTemp #1570
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
& Fix Report KK1 #2439
2026-04-23 16:31:22 +07:00
adisak
194d79bf04 #231 และ #2438 checkpoint 2026-04-21 17:37:17 +07:00
7e3982a96d fixed calculate tenure (สูตรคำนวนอายุราชการจาก diff date)
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m6s
2026-04-20 18:20:20 +07:00
5e52206987 update
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m6s
2026-04-20 17:23:15 +07:00
f1c8ecf699 insert position to profile
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m8s
2026-04-20 16:01:38 +07:00
adisak
28b5408d5b #2427 and migration 2026-04-20 08:05:16 +07:00
harid
7f3408e2f5 API permission with acting positions
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m36s
2026-04-17 14:18:54 +07:00
99bd789702 fixed#230 noti เพิ่มลิ้งค์ไปหน้ารายละเอียดแก้ไขข้อมูล "ขอแก้ไขทะเบียนประวัติ"
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m37s
2026-04-17 14:00:00 +07:00
harid
e7a973b764 fix issues #2428 #2383
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-04-16 15:59:36 +07:00
24 changed files with 2277 additions and 587 deletions

View file

@ -0,0 +1,154 @@
-- =====================================================
-- Update position fields in profile table
-- อัพเดทฟิลด์ตำแหน่งในตาราง profile
--
-- Fields:
-- - positionField (สายงาน)
-- - posExecutive (ตำแหน่งทางการบริหาร)
-- - positionArea (ด้าน/สาขา)
-- - positionExecutiveField (ด้านทางการบริหาร)
-- - posMasterNo (เลขที่ตำแหน่ง) - format: orgShortName + space + number
-- - org (สังกัด)
--
-- Run each query separately to verify results
-- =====================================================
USE hrms_organization;
-- 1. Update positionField (สายงาน)
UPDATE profile p
INNER JOIN posMaster pm ON pm.current_holderId = p.id
INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0
INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1
SET p.positionField = pos.positionField
WHERE p.positionField IS NULL;
-- 2. Update posExecutive (ตำแหน่งทางการบริหาร)
UPDATE profile p
INNER JOIN posMaster pm ON pm.current_holderId = p.id
INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0
INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1
INNER JOIN posExecutive pe ON pos.posExecutiveId = pe.id
SET p.posExecutive = pe.posExecutiveName
WHERE p.posExecutive IS NULL;
-- 3. Update positionArea (ด้าน/สาขา)
UPDATE profile p
INNER JOIN posMaster pm ON pm.current_holderId = p.id
INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0
INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1
SET p.positionArea = pos.positionArea
WHERE p.positionArea IS NULL;
-- 4. Update positionExecutiveField (ด้านทางการบริหาร)
UPDATE profile p
INNER JOIN posMaster pm ON pm.current_holderId = p.id
INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0
INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1
SET p.positionExecutiveField = pos.positionExecutiveField
WHERE p.positionExecutiveField IS NULL;
-- 5. Update posMasterNo (เลขที่ตำแหน่ง) - format: orgShortName + space + number
UPDATE profile p
INNER JOIN posMaster pm ON pm.current_holderId = p.id
INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0
LEFT JOIN orgRoot r ON pm.orgRootId = r.id
LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id
LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id
LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id
LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id
SET p.posMasterNo = TRIM(CONCAT(
CASE
WHEN pm.orgChild1Id IS NULL THEN r.orgRootShortName
WHEN pm.orgChild2Id IS NULL THEN c1.orgChild1ShortName
WHEN pm.orgChild3Id IS NULL THEN c2.orgChild2ShortName
WHEN pm.orgChild4Id IS NULL THEN c3.orgChild3ShortName
ELSE c4.orgChild4ShortName
END,
' ',
CONCAT_WS('', pm.posMasterNoPrefix, pm.posMasterNo, pm.posMasterNoSuffix)
))
WHERE p.posMasterNo IS NULL;
-- 6. Update org (สังกัด) - combine all org levels
UPDATE profile p
INNER JOIN posMaster pm ON pm.current_holderId = p.id
INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0
LEFT JOIN orgRoot r ON pm.orgRootId = r.id
LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id
LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id
LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id
LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id
SET p.org = TRIM(CONCAT_WS(
CHAR(10),
c4.orgChild4Name,
c3.orgChild3Name,
c2.orgChild2Name,
c1.orgChild1Name,
r.orgRootName
))
WHERE p.org IS NULL;
-- =====================================================
-- เช็คผลลัพธ์ (Check results)
-- =====================================================
-- เช็คจำนวนที่ update ได้
SELECT
COUNT(CASE WHEN positionField IS NOT NULL THEN 1 END) AS has_positionField,
COUNT(CASE WHEN posExecutive IS NOT NULL THEN 1 END) AS has_posExecutive,
COUNT(CASE WHEN positionArea IS NOT NULL THEN 1 END) AS has_positionArea,
COUNT(CASE WHEN positionExecutiveField IS NOT NULL THEN 1 END) AS has_positionExecutiveField,
COUNT(CASE WHEN posMasterNo IS NOT NULL THEN 1 END) AS has_posMasterNo,
COUNT(CASE WHEN org IS NOT NULL THEN 1 END) AS has_org
FROM profile;
-- =====================================================
-- SELECT query สำหรับทดสอบก่อนรัน (Test before run)
-- =====================================================
SELECT
p.id,
p.firstName,
p.lastName,
p.citizenId,
p.positionField as old_positionField,
p.posExecutive as old_posExecutive,
p.positionArea as old_positionArea,
p.positionExecutiveField as old_positionExecutiveField,
p.posMasterNo as old_posMasterNo,
p.org as old_org,
pos.positionField as new_positionField,
pe.posExecutiveName as new_posExecutive,
pos.positionArea as new_positionArea,
pos.positionExecutiveField as new_positionExecutiveField,
TRIM(CONCAT(
CASE
WHEN pm.orgChild1Id IS NULL THEN r.orgRootShortName
WHEN pm.orgChild2Id IS NULL THEN c1.orgChild1ShortName
WHEN pm.orgChild3Id IS NULL THEN c2.orgChild2ShortName
WHEN pm.orgChild4Id IS NULL THEN c3.orgChild3ShortName
ELSE c4.orgChild4ShortName
END,
' ',
pm.posMasterNo
)) as new_posMasterNo,
TRIM(CONCAT_WS(CHAR(10), c4.orgChild4Name, c3.orgChild3Name, c2.orgChild2Name, c1.orgChild1Name, r.orgRootName)) as new_org
FROM profile p
INNER JOIN posMaster pm ON pm.current_holderId = p.id
INNER JOIN orgRevision oRev ON pm.orgRevisionId = oRev.id AND oRev.orgRevisionIsCurrent = 1 AND oRev.orgRevisionIsDraft = 0
INNER JOIN position pos ON pos.posMasterId = pm.id AND pos.positionIsSelected = 1
LEFT JOIN posExecutive pe ON pos.posExecutiveId = pe.id
LEFT JOIN orgRoot r ON pm.orgRootId = r.id
LEFT JOIN orgChild1 c1 ON pm.orgChild1Id = c1.id
LEFT JOIN orgChild2 c2 ON pm.orgChild2Id = c2.id
LEFT JOIN orgChild3 c3 ON pm.orgChild3Id = c3.id
LEFT JOIN orgChild4 c4 ON pm.orgChild4Id = c4.id
-- ใส่ WHERE ทดสอบ 1 คน (Test 1 person)
WHERE p.id = 'ใส่ profile_id ที่ต้องการทดสอบ'
-- หรือทดสอบ 10 คน (Test 10 persons)
-- LIMIT 10;

View file

@ -48,6 +48,7 @@ import {
import { Position } from "../entities/Position";
import { PosMaster } from "../entities/PosMaster";
import { EmployeePosition } from "../entities/EmployeePosition";
import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting";
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { ProfileDiscipline } from "../entities/ProfileDiscipline";
import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory";
@ -3660,6 +3661,7 @@ export class CommandController extends Controller {
const posMaster = await this.posMasterRepository.findOne({
where: { id: item.posmasterId },
relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"],
});
if (posMaster == null)
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
@ -3715,14 +3717,22 @@ export class CommandController extends Controller {
id: item.positionId,
posMasterId: item.posmasterId,
},
relations: ["posExecutive"],
});
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (positionNew != null) {
positionNew.positionIsSelected = true;
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
profile.posMasterNo = getPosMasterNo(posMaster);
profile.org = getOrgFullName(posMaster);
if(!posMaster.isSit){
profile.posLevelId = positionNew.posLevelId;
profile.posTypeId = positionNew.posTypeId;
profile.position = positionNew.positionName;
profile.positionField = positionNew.positionField ?? null;
profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null;
profile.positionArea = positionNew.positionArea ?? null;
profile.positionExecutiveField = positionNew.positionExecutiveField ?? null;
}
profile.amount = item.amount ?? null;
profile.amountSpecial = item.amountSpecial ?? null;
@ -6500,6 +6510,7 @@ export class CommandController extends Controller {
relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"],
});
let _oldInsigniaIds: string[] = [];
let _oldSalaries: any[] = [];
//ลูกจ้างประจำ หรือ บุคคลภายนอก
if (!profile) {
//กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม
@ -6608,6 +6619,11 @@ export class CommandController extends Controller {
profile.isLeave &&
["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType)
) {
//ดึง profileSalary เดิม
_oldSalaries = await this.salaryRepo.find({
where: { profileId: profile.id },
order: { order: "ASC" },
});
if (profile.profileInsignias.length > 0) {
_oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? [];
}
@ -6846,6 +6862,23 @@ export class CommandController extends Controller {
await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req });
}
//Salary
//insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ
if (_oldSalaries.length > 0) {
await Promise.all(
_oldSalaries.map(async (oldSal) => {
const profileSal: any = new ProfileSalary();
Object.assign(profileSal, { ...oldSal, ...meta });
const salaryHistory = new ProfileSalaryHistory();
Object.assign(salaryHistory, { ...profileSal, id: undefined });
profileSal.profileId = profile.id;
await this.salaryRepo.save(profileSal, { data: req });
setLogDataDiff(req, { before, after: profileSal });
salaryHistory.profileSalaryId = profileSal.id;
await this.salaryHistoryRepo.save(salaryHistory, { data: req });
}),
);
}
//insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว
if (item.bodySalarys && item.bodySalarys != null) {
const dest_item = await this.salaryRepo.findOne({
where: { profileId: profile.id },
@ -6876,7 +6909,7 @@ export class CommandController extends Controller {
where: {
id: item.bodyPosition.posmasterId,
},
relations: { orgRevision: true }
relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true }
});
// เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่
@ -6893,9 +6926,8 @@ export class CommandController extends Controller {
orgRevisionIsDraft: false
}
},
relations: { orgRevision: true }
});
}
relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true }
}); }
if (posMaster == null)
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
@ -6983,20 +7015,28 @@ export class CommandController extends Controller {
id: item.bodyPosition.positionId,
posMasterId: posMaster.id,
},
relations: ["posExecutive"],
});
}
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (positionNew != null) {
positionNew.positionIsSelected = true;
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
profile.posMasterNo = getPosMasterNo(posMaster);
profile.org = getOrgFullName(posMaster);
if(!posMaster.isSit){
profile.posLevelId = positionNew.posLevelId;
profile.posTypeId = positionNew.posTypeId;
profile.position = positionNew.positionName;
profile.positionField = positionNew.positionField ?? null;
profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null;
profile.positionArea = positionNew.positionArea ?? null;
profile.positionExecutiveField = positionNew.positionExecutiveField ?? null;
// profile.dateStart = new Date();
await this.profileRepository.save(profile, { data: req });
setLogDataDiff(req, { before, after: profile });
}
await this.profileRepository.save(profile, { data: req });
setLogDataDiff(req, { before, after: profile });
await this.positionRepository.save(positionNew, { data: req });
}
// await CreatePosMasterHistoryOfficer(posMaster.id, req);

View file

@ -15,6 +15,7 @@ import {
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { addLogSequence } from "../interfaces/utils";
import HttpSuccess from "../interfaces/http-success";
interface CachedToken {
token: string;
@ -88,7 +89,8 @@ export class ExRetirementController extends Controller {
},
});
return res.data;
// return res.data;
return new HttpSuccess(res.data.data);
} catch (error: any) {
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
TokenCache.delete(`${clientId}:${clientSecret}`);

View file

@ -1,4 +1,4 @@
import { Controller, Post, Route, Security, Tags, Request, UploadedFile } from "tsoa";
import { Controller, Post, Route, Security, Tags, Request, UploadedFile, Path } from "tsoa";
import { AppDataSource } from "../database/data-source";
import { In, IsNull, LessThanOrEqual, Not, Between } from "typeorm";
import HttpSuccess from "../interfaces/http-success";
@ -105,6 +105,7 @@ import { positionOfficer } from "../entities/mis/positionOfficer";
import { ProvinceMaster } from "../entities/ProvinceMaster";
import { SubDistrictMaster } from "../entities/SubDistrictMaster";
import { DistrictMaster } from "../entities/DistrictMaster";
import { RequestWithUser } from "../middlewares/user";
@Route("api/v1/org/upload")
@Tags("UPLOAD")
@Security("bearerAuth")
@ -6815,4 +6816,523 @@ export class ImportDataController extends Controller {
// await repo.save(entities);
// return entities;
// }
/**
* @summary Import ProfileSalaryTemp
* @param profileId Id
* @param file Excel file with salary history data
*/
@Post("office-profileSalaryTemp/{profileId}")
@UseInterceptors(FileInterceptor("file"))
async UploadProfileSalaryTemp(
@Path() profileId: string,
@Request() req: RequestWithUser,
@UploadedFile() file: Express.Multer.File,
) {
if (!profileId) {
throw new Error("profileId is required");
}
// อ่านไฟล์ Excel ก่อน (นอก transaction)
const workbook = xlsx.read(file.buffer, { type: "buffer" });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][];
let salaryTemps: ProfileSalaryTemp[] = [];
let dateTime = new Date();
// เริ่มจาก index 1 เพื่อข้าม header row
for (let i = 1; i < getExcel.length; i++) {
const row = getExcel[i];
// ข้าม empty rows
if (!row || row.length === 0) {
continue;
}
// ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง)
if (!row[0]) {
continue;
}
const salaryTemp = new ProfileSalaryTemp();
// ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number
const parseExcelDate = (value: any): Date | null => {
if (!value) return null;
// กรณี 1: Excel serial number (ตัวเลข)
if (typeof value === "number") {
// Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900
// แปลงเป็น JavaScript Date (epoch 1970)
let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000));
// ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500)
if (jsDate.getFullYear() > 2500) {
const newYear = jsDate.getFullYear() - 543;
jsDate = new Date(
newYear,
jsDate.getMonth(),
jsDate.getDate(),
jsDate.getHours(),
jsDate.getMinutes(),
jsDate.getSeconds(),
jsDate.getMilliseconds(),
);
}
return jsDate;
}
// กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy)
const dateStr = value.toString().trim();
// ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่
if (/^\d+$/.test(dateStr)) {
const serialNum = parseInt(dateStr);
let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000));
// ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500)
if (jsDate.getFullYear() > 2500) {
const newYear = jsDate.getFullYear() - 543;
jsDate = new Date(
newYear,
jsDate.getMonth(),
jsDate.getDate(),
jsDate.getHours(),
jsDate.getMinutes(),
jsDate.getSeconds(),
jsDate.getMilliseconds(),
);
}
return jsDate;
}
// String format ปกติ (dd/mm/yyyy)
const dateParts = dateStr.split("/");
if (dateParts.length === 3) {
// แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก
const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0");
const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0");
let year = parseInt(dateParts[2].trim());
if (year > 2500) {
year -= 543;
}
const result = new Date(`${year}-${month}-${day}`);
return result;
}
return null;
};
// Index 1: วันที่คำสั่งมีผล
let commandDateAffect: Date | null = null;
if (row[1]) {
commandDateAffect = parseExcelDate(row[1]);
}
// Index 25: วันที่ลงนาม
let commandDateSign: Date | null = null;
if (row[25]) {
commandDateSign = parseExcelDate(row[25]);
}
// Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column
// ข้อมูลระบบ
salaryTemp.profileId = profileId;
salaryTemp.profileEmployeeId = null as any;
// Index 0: ลำดับ
salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any);
// Index 1: วันที่คำสั่งมีผล
salaryTemp.commandDateAffect = commandDateAffect as any;
// Index 2: ตำแหน่งในสายงาน
salaryTemp.positionName = row[2] || null;
// Index 3: ตำแหน่งประเภท
salaryTemp.positionType = row[3] || null;
// Index 4: ระดับ
salaryTemp.positionLevel = row[4] || null;
// Index 5: ระดับซี
salaryTemp.positionCee = row[5] || null;
// Index 6: สายงาน
salaryTemp.positionLine = row[6] || null;
// Index 7: ด้าน/สาขา
salaryTemp.positionPathSide = row[7] || null;
// Index 8: ตำแหน่งทางการบริหาร
salaryTemp.positionExecutive = row[8] || null;
// Index 9: ด้านทางการบริหาร
salaryTemp.positionExecutiveField = row[9] || null;
// Index 10: เงินเดือน
salaryTemp.amount = row[10] || 0;
// Index 11: เงินค่าตอบแทนรายเดือน
salaryTemp.mouthSalaryAmount = row[11] || 0;
// Index 12: เงินประจำตำแหน่ง
salaryTemp.positionSalaryAmount = row[12] || 0;
// Index 13: เงินค่าตอบแทนพิเศษ
salaryTemp.amountSpecial = row[13] || 0;
// Index 14: หน่วยงาน
salaryTemp.orgRoot = row[14] || null;
// Index 15: ส่วนราชการระดับ 1
salaryTemp.orgChild1 = row[15] || null;
// Index 16: ส่วนราชการระดับ 2
salaryTemp.orgChild2 = row[16] || null;
// Index 17: ส่วนราชการระดับ 3
salaryTemp.orgChild3 = row[17] || null;
// Index 18: ส่วนราชการระดับ 4
salaryTemp.orgChild4 = row[18] || null;
// Index 19: ตัวย่อเลขที่ตำแหน่ง
salaryTemp.posNoAbb = row[19] || null;
// Index 20: เลขที่ตำแหน่ง
salaryTemp.posNo = row[20] ? row[20].toString() : null;
// Index 21: หน่วยงานที่ออกคำสั่ง
salaryTemp.posNumCodeSit = row[21] || null;
// Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง
salaryTemp.posNumCodeSitAbb = row[22] || null;
// Index 23: เลขที่คำสั่ง
salaryTemp.commandNo = row[23] || null;
// Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.)
let commandYearValue: number | null = null;
if (row[24]) {
commandYearValue = parseInt(row[24].toString());
// ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ.
if (commandYearValue > 2500) {
commandYearValue -= 543;
}
}
salaryTemp.commandYear = commandYearValue as any;
// Index 25: วันที่ลงนาม (แปลงแล้ว)
salaryTemp.commandDateSign = commandDateSign as any;
// Index 26: ประเภทคำสั่ง
salaryTemp.commandName = row[26] || null;
// Index 27: หมายเหตุ
salaryTemp.remark = row[27] || null;
// Index 28: commandId
salaryTemp.commandId = row[28] || null;
// Index 29: commandCode
salaryTemp.commandCode = row[29] || null;
// ข้อมูลระบบ
salaryTemp.isDelete = false;
salaryTemp.isEdit = false;
salaryTemp.isGovernment = false;
salaryTemp.isEntry = false;
salaryTemp.createdAt = dateTime;
salaryTemp.createdUserId = req.user?.sub || "";
salaryTemp.createdFullName = req.user?.name || "System Administrator";
salaryTemp.lastUpdatedAt = dateTime;
salaryTemp.lastUpdateUserId = req.user?.sub || "";
salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator";
// 12,15,16 isGovernment = false & dateGovernment = salaryTemp.commandDateAffect
if (["12", "15", "16"].includes(salaryTemp.commandCode ?? "")) {
salaryTemp.isGovernment = false;
salaryTemp.dateGovernment = salaryTemp.commandDateAffect;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = salaryTemp.commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(salaryTemp.commandCode ?? "")) {
salaryTemp.isGovernment = true;
salaryTemp.dateGovernment = salaryTemp.commandDateAffect;
}
salaryTemps.push(salaryTemp);
}
// ใช้ Transaction เพื่อความปลอดภัย
await AppDataSource.transaction(async (transactionalEntityManager) => {
// ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileId นั้น
await transactionalEntityManager.delete(ProfileSalaryTemp, { profileId });
// Insert ข้อมูลใหม่
await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps);
});
return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length });
}
/**
* @summary Import ProfileSalaryTemp
* @param profileEmployeeId Id
* @param file Excel file with salary history data
*/
@Post("employee-profileSalaryTemp/{profileEmployeeId}")
@UseInterceptors(FileInterceptor("file"))
async UploadProfileEmployeeSalaryTemp(
@Path() profileEmployeeId: string,
@Request() req: RequestWithUser,
@UploadedFile() file: Express.Multer.File,
) {
if (!profileEmployeeId) {
throw new Error("profileEmployeeId is required");
}
// อ่านไฟล์ Excel ก่อน (นอก transaction)
const workbook = xlsx.read(file.buffer, { type: "buffer" });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][];
let salaryTemps: ProfileSalaryTemp[] = [];
let dateTime = new Date();
// เริ่มจาก index 1 เพื่อข้าม header row
for (let i = 1; i < getExcel.length; i++) {
const row = getExcel[i];
// ข้าม empty rows
if (!row || row.length === 0) {
continue;
}
// ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง)
if (!row[0]) {
continue;
}
const salaryTemp = new ProfileSalaryTemp();
// ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number
const parseExcelDate = (value: any): Date | null => {
if (!value) return null;
// กรณี 1: Excel serial number (ตัวเลข)
if (typeof value === "number") {
// Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900
// แปลงเป็น JavaScript Date (epoch 1970)
let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000));
// ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500)
if (jsDate.getFullYear() > 2500) {
const newYear = jsDate.getFullYear() - 543;
jsDate = new Date(
newYear,
jsDate.getMonth(),
jsDate.getDate(),
jsDate.getHours(),
jsDate.getMinutes(),
jsDate.getSeconds(),
jsDate.getMilliseconds(),
);
}
return jsDate;
}
// กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy)
const dateStr = value.toString().trim();
// ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่
if (/^\d+$/.test(dateStr)) {
const serialNum = parseInt(dateStr);
let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000));
// ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500)
if (jsDate.getFullYear() > 2500) {
const newYear = jsDate.getFullYear() - 543;
jsDate = new Date(
newYear,
jsDate.getMonth(),
jsDate.getDate(),
jsDate.getHours(),
jsDate.getMinutes(),
jsDate.getSeconds(),
jsDate.getMilliseconds(),
);
}
return jsDate;
}
// String format ปกติ (dd/mm/yyyy)
const dateParts = dateStr.split("/");
if (dateParts.length === 3) {
// แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก
const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0");
const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0");
let year = parseInt(dateParts[2].trim());
if (year > 2500) {
year -= 543;
}
const result = new Date(`${year}-${month}-${day}`);
return result;
}
return null;
};
// Index 1: วันที่คำสั่งมีผล
let commandDateAffect: Date | null = null;
if (row[1]) {
commandDateAffect = parseExcelDate(row[1]);
}
// Index 25: วันที่ลงนาม
let commandDateSign: Date | null = null;
if (row[25]) {
commandDateSign = parseExcelDate(row[25]);
}
// Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column
// ข้อมูลระบบ
salaryTemp.profileEmployeeId = profileEmployeeId;
salaryTemp.profileId = null as any;
// Index 0: ลำดับ
salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any);
// Index 1: วันที่คำสั่งมีผล
salaryTemp.commandDateAffect = commandDateAffect as any;
// Index 2: ตำแหน่งในสายงาน
salaryTemp.positionName = row[2] || null;
// Index 3: ตำแหน่งประเภท
salaryTemp.positionType = row[3] || null;
// Index 4: ระดับ
salaryTemp.positionLevel = row[4] || null;
// Index 5: ระดับซี
salaryTemp.positionCee = row[5] || null;
// Index 6: สายงาน
salaryTemp.positionLine = row[6] || null;
// Index 7: ด้าน/สาขา
salaryTemp.positionPathSide = row[7] || null;
// Index 8: ตำแหน่งทางการบริหาร
salaryTemp.positionExecutive = row[8] || null;
// Index 9: ด้านทางการบริหาร
salaryTemp.positionExecutiveField = row[9] || null;
// Index 10: เงินเดือน
salaryTemp.amount = row[10] || 0;
// Index 11: เงินค่าตอบแทนรายเดือน
salaryTemp.mouthSalaryAmount = row[11] || 0;
// Index 12: เงินประจำตำแหน่ง
salaryTemp.positionSalaryAmount = row[12] || 0;
// Index 13: เงินค่าตอบแทนพิเศษ
salaryTemp.amountSpecial = row[13] || 0;
// Index 14: หน่วยงาน
salaryTemp.orgRoot = row[14] || null;
// Index 15: ส่วนราชการระดับ 1
salaryTemp.orgChild1 = row[15] || null;
// Index 16: ส่วนราชการระดับ 2
salaryTemp.orgChild2 = row[16] || null;
// Index 17: ส่วนราชการระดับ 3
salaryTemp.orgChild3 = row[17] || null;
// Index 18: ส่วนราชการระดับ 4
salaryTemp.orgChild4 = row[18] || null;
// Index 19: ตัวย่อเลขที่ตำแหน่ง
salaryTemp.posNoAbb = row[19] || null;
// Index 20: เลขที่ตำแหน่ง
salaryTemp.posNo = row[20] ? row[20].toString() : null;
// Index 21: หน่วยงานที่ออกคำสั่ง
salaryTemp.posNumCodeSit = row[21] || null;
// Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง
salaryTemp.posNumCodeSitAbb = row[22] || null;
// Index 23: เลขที่คำสั่ง
salaryTemp.commandNo = row[23] || null;
// Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.)
let commandYearValue: number | null = null;
if (row[24]) {
commandYearValue = parseInt(row[24].toString());
// ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ.
if (commandYearValue > 2500) {
commandYearValue -= 543;
}
}
salaryTemp.commandYear = commandYearValue as any;
// Index 25: วันที่ลงนาม (แปลงแล้ว)
salaryTemp.commandDateSign = commandDateSign as any;
// Index 26: ประเภทคำสั่ง
salaryTemp.commandName = row[26] || null;
// Index 27: หมายเหตุ
salaryTemp.remark = row[27] || null;
// Index 28: commandId
salaryTemp.commandId = row[28] || null;
// Index 29: commandCode
salaryTemp.commandCode = row[29] || null;
// ข้อมูลระบบ
salaryTemp.isDelete = false;
salaryTemp.isEdit = false;
salaryTemp.isGovernment = false;
salaryTemp.isEntry = false;
salaryTemp.createdAt = dateTime;
salaryTemp.createdUserId = req.user?.sub || "";
salaryTemp.createdFullName = req.user?.name || "System Administrator";
salaryTemp.lastUpdatedAt = dateTime;
salaryTemp.lastUpdateUserId = req.user?.sub || "";
salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator";
// 12,15,16 isGovernment = false & dateGovernment = salaryTemp.commandDateAffect
if (["12", "15", "16"].includes(salaryTemp.commandCode ?? "")) {
salaryTemp.isGovernment = false;
salaryTemp.dateGovernment = salaryTemp.commandDateAffect;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = salaryTemp.commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(salaryTemp.commandCode ?? "")) {
salaryTemp.isGovernment = true;
salaryTemp.dateGovernment = salaryTemp.commandDateAffect;
}
salaryTemps.push(salaryTemp);
}
// ใช้ Transaction เพื่อความปลอดภัย
await AppDataSource.transaction(async (transactionalEntityManager) => {
// ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileEmployeeId นั้น
await transactionalEntityManager.delete(ProfileSalaryTemp, { profileEmployeeId });
// Insert ข้อมูลใหม่
await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps);
});
return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length });
}
}

View file

@ -315,4 +315,81 @@ export class KeycloakSyncController extends Controller {
...result,
});
}
/**
* Sync profiles with missing empType for a specific month (Admin only)
*
* @summary Find profiles updated in specified month with missing empType in Keycloak and sync them (ADMIN)
*
* @description
* This endpoint will:
* - List profiles from Profile table where lastUpdatedAt falls within the specified month
* - For each profile, check Keycloak if empType attribute is empty/null
* - If empType is empty, sync the profile using existing sync logic
* - Return summary of sync results
*
* Features:
* - Dry run mode (dryRun=true) to check without syncing
* - Configurable concurrency for parallel processing
* - Rate limiting to avoid overwhelming Keycloak
* - Detailed error reporting
* - Idempotent (can be safely re-run)
*
* @param {request} request Request body containing month parameter
* @param dryRun - If true, only check without syncing (default: false)
* @param concurrency - Number of parallel operations (default: 5)
* @param rateLimit - Requests per second limit (default: 10)
*/
@Post("sync-missing-emptype")
@Response<HttpError>(HttpStatus.BAD_REQUEST, "Invalid month format")
@Response<HttpError>(HttpStatus.INTERNAL_SERVER_ERROR, "Sync operation failed")
async syncMissingEmpType(
@Body() request: {
month: string;
profileType?: "PROFILE" | "PROFILE_EMPLOYEE";
},
@Query() dryRun: boolean = false,
@Query() concurrency: number = 5,
@Query() rateLimit: number = 10,
) {
const { month, profileType = "PROFILE" } = request;
// Validate month format (YYYY-MM)
const monthRegex = /^\d{4}-\d{2}$/;
if (!monthRegex.test(month)) {
throw new HttpError(HttpStatus.BAD_REQUEST, "รูปแบบเดือนไม่ถูกต้อง ต้องเป็น YYYY-MM");
}
// Validate profileType
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
);
}
// Validate concurrency
if (concurrency < 1 || concurrency > 20) {
throw new HttpError(HttpStatus.BAD_REQUEST, "concurrency ต้องอยู่ระหว่าง 1 ถึง 20");
}
// Validate rateLimit
if (rateLimit < 1 || rateLimit > 50) {
throw new HttpError(HttpStatus.BAD_REQUEST, "rateLimit ต้องอยู่ระหว่าง 1 ถึง 50");
}
// Execute sync
const result = await this.keycloakAttributeService.syncMissingEmpTypeByMonth({
month,
profileType,
dryRun,
concurrency,
rateLimit,
});
return new HttpSuccess({
message: `Sync ${dryRun ? "check " : ""}เสร็จสิ้น`,
...result,
});
}
}

View file

@ -66,7 +66,7 @@ import {
import { orgStructureCache } from "../utils/OrgStructureCache";
import { OrgIdMapping, AllOrgMappings, SavePosMasterHistory } from "../interfaces/OrgMapping";
import { OrgPermissionData, NodeLevel } from "../interfaces/OrgTypes";
import { formatPosMaster, generateLabelName, filterPosMasters } from "../utils/org-formatting";
import { formatPosMaster, generateLabelName, filterPosMasters, getPosMasterNo, getOrgFullName } from "../utils/org-formatting";
@Route("api/v1/org")
@Tags("Organization")
@ -8933,13 +8933,25 @@ export class OrganizationController extends Controller {
const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any;
// Collect profile update for the selected position
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
if (nextHolderId != null && draftPos.positionIsSelected) {
const _null: any = null;
profileUpdates.set(nextHolderId, {
posMasterNo: draftPosMaster ? (getPosMasterNo(draftPosMaster as PosMaster) ?? _null) : _null,
org: draftPosMaster ? (getOrgFullName(draftPosMaster as PosMaster) ?? _null) : _null,
});
}
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (nextHolderId != null && draftPos.positionIsSelected && !draftPosMaster?.isSit) {
profileUpdates.set(nextHolderId, {
position: draftPos.positionName,
posTypeId: draftPos.posTypeId,
posLevelId: draftPos.posLevelId,
});
const existing = profileUpdates.get(nextHolderId) || {};
existing.position = draftPos.positionName;
existing.posTypeId = draftPos.posTypeId;
existing.posLevelId = draftPos.posLevelId;
existing.positionField = draftPos.positionField ?? null;
existing.posExecutive = (draftPos as any).posExecutive?.posExecutiveName ?? null;
existing.positionArea = draftPos.positionArea ?? null;
existing.positionExecutiveField = draftPos.positionExecutiveField ?? null;
profileUpdates.set(nextHolderId, existing);
if (draftPosMaster && draftPosMaster.ancestorDNA) {
// Find the selected position from draft positions
const selectedPos =

View file

@ -15,6 +15,7 @@ import permission from "../interfaces/permission";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { OrgRevision } from "../entities/OrgRevision";
import { actingPositionService } from "../services/ActingPositionService";
const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = process.env.REDIS_PORT;
@ -254,6 +255,64 @@ export class PermissionController extends Controller {
return new HttpSuccess(res);
}
/**
* API permission with acting positions
* @summary permission with acting positions (dotnet api)
* @param {string} action action
* @param {string} system authSysId
*/
@Get("dotnet-acting/{action}/{system}")
public async dotnetActing(
@Request() req: RequestWithUser,
@Path() action: string,
@Path() system: string,
) {
if (!["CREATE", "DELETE", "GET", "LIST", "UPDATE"].includes(action)) {
throw new HttpError(HttpStatus.NOT_FOUND, "Action ไม่ถูกต้อง");
}
// ดึง privilege ตามปกติ
let privilege = await new permission().Permission(req, system.toLocaleUpperCase(), action);
// ดึงข้อมูล profile และ orgRevision
let profile: any = await this.profileRepo.findOne({
select: ["id"],
where: { keycloak: req.user.sub },
});
if (!profile) {
profile = await this.profileEmployeeRepo.findOne({
select: ["id"],
where: { keycloak: req.user.sub },
});
if (!profile) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ");
}
}
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
// ดึงข้อมูลตำแหน่งที่รักษาการ
const actingData = await actingPositionService.getActingPositionsWithPrivilege(
profile.id,
orgRevision?.id,
action,
system.toLocaleUpperCase()
);
// ส่งค่ากลับเหมือน dotnet endpoint แต่เพิ่ม isAct และ posMasterActs
return new HttpSuccess({
privilege,
isAct: actingData.isAct,
posMasterActs: actingData.posMasterActs,
});
}
/**
* API permission (dotnet api)
* @summary permission (dotnet api)

View file

@ -39,6 +39,7 @@ import { AuthRole } from "../entities/AuthRole";
import { RequestWithUser } from "../middlewares/user";
import permission from "../interfaces/permission";
import { resolveNodeLevel, setLogDataDiff } from "../interfaces/utils";
import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting";
import { PosMasterAssign } from "../entities/PosMasterAssign";
import { Assign } from "../entities/Assign";
import { ProfileEmployee } from "../entities/ProfileEmployee";
@ -1256,7 +1257,15 @@ export class PositionController extends Controller {
) {
await new permission().PermissionUpdate(request, "SYS_ORG");
const posMaster = await this.posMasterRepository.findOne({
relations: ["positions", "orgRevision"],
relations: [
"positions",
"orgRevision",
"orgRoot",
"orgChild1",
"orgChild2",
"orgChild3",
"orgChild4",
],
where: { id: id },
});
if (!posMaster) {
@ -1451,6 +1460,17 @@ export class PositionController extends Controller {
}),
);
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
if (posMaster.orgRevision?.orgRevisionIsCurrent == true && posMaster.current_holderId) {
const _profile = await this.profileRepository.findOne({
where: { id: posMaster.current_holderId },
});
if (_profile) {
_profile.posMasterNo = getPosMasterNo(posMaster);
_profile.org = getOrgFullName(posMaster);
await this.profileRepository.save(_profile);
}
}
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (posMaster.orgRevision?.orgRevisionIsCurrent == true && !posMaster.isSit) {
const _position = requestBody.positions.find((p) => p.positionIsSelected == true);
@ -1463,6 +1483,10 @@ export class PositionController extends Controller {
_profile.position = _position.posDictName ?? _null;
_profile.posTypeId = _position.posTypeId;
_profile.posLevelId = _position.posLevelId;
_profile.positionField = _position.posDictField ?? _null;
_profile.posExecutive = _position.posExecutiveId ?? _null;
_profile.positionArea = _position.posDictArea ?? _null;
_profile.positionExecutiveField = _position.posDictExecutiveField ?? _null;
await this.profileRepository.save(_profile);
}
}
@ -2387,16 +2411,16 @@ export class PositionController extends Controller {
? "posMaster.orgRootId IN (:...root)"
: "posMaster.orgRootId is null"
: "1=1",
{ root: _data.root }
{ root: _data.root },
)
.andWhere(
_data.child1 != undefined && _data.child1 != null
? _data.child1[0] != null
? "posMaster.orgChild1Id IN (:...child1)"
// : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
: `posMaster.orgChild1Id is null`
: // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
`posMaster.orgChild1Id is null`
: "1=1",
{ child1: _data.child1 }
{ child1: _data.child1 },
)
.andWhere(
_data.child2 != undefined && _data.child2 != null
@ -2427,26 +2451,27 @@ export class PositionController extends Controller {
{
child4: _data.child4,
},
)
);
// .andWhere(checkChildConditions)
// .andWhere(typeCondition)
// .andWhere(revisionCondition);
if (body.keyword != null && body.keyword != "") {
query.orWhere(
new Brackets((qb) => {
qb.andWhere(
body.keyword != null && body.keyword != ""
? body.isAll == false
? searchShortName
: `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'`
: "1=1",
)
.andWhere(checkChildConditions)
.andWhere(typeCondition)
.andWhere(revisionCondition);
}),
)
query
.orWhere(
new Brackets((qb) => {
qb.andWhere(
body.keyword != null && body.keyword != ""
? body.isAll == false
? searchShortName
: `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'`
: "1=1",
)
.andWhere(checkChildConditions)
.andWhere(typeCondition)
.andWhere(revisionCondition);
}),
)
.orWhere(
new Brackets((qb) => {
qb.andWhere(
@ -2955,50 +2980,50 @@ export class PositionController extends Controller {
const type0LastPosMasterNo =
requestBody.type == 0
? await this.posMasterRepository.find({
where: {
orgRootId: requestBody.id,
orgChild1Id: IsNull(),
},
})
where: {
orgRootId: requestBody.id,
orgChild1Id: IsNull(),
},
})
: [];
const type1LastPosMasterNo =
requestBody.type == 1
? await this.posMasterRepository.find({
where: {
orgChild1Id: requestBody.id,
orgChild2Id: IsNull(),
},
})
where: {
orgChild1Id: requestBody.id,
orgChild2Id: IsNull(),
},
})
: [];
const type2LastPosMasterNo =
requestBody.type == 2
? await this.posMasterRepository.find({
where: {
orgChild2Id: requestBody.id,
orgChild3Id: IsNull(),
},
})
where: {
orgChild2Id: requestBody.id,
orgChild3Id: IsNull(),
},
})
: [];
const type3LastPosMasterNo =
requestBody.type == 3
? await this.posMasterRepository.find({
where: {
orgChild3Id: requestBody.id,
orgChild4Id: IsNull(),
},
})
where: {
orgChild3Id: requestBody.id,
orgChild4Id: IsNull(),
},
})
: [];
const type4LastPosMasterNo =
requestBody.type == 4
? await this.posMasterRepository.find({
where: {
orgChild4Id: requestBody.id,
},
})
where: {
orgChild4Id: requestBody.id,
},
})
: [];
const allLastPosMasterNo = [
@ -3793,7 +3818,7 @@ export class PositionController extends Controller {
await new permission().PermissionUpdate(request, "SYS_ORG");
const dataMaster = await this.posMasterRepository.findOne({
where: { id: requestBody.posMaster },
relations: ["positions"],
relations: ["positions", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"],
});
if (!dataMaster) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
@ -3825,16 +3850,24 @@ export class PositionController extends Controller {
if (_profile) {
let _position = await this.positionRepository.findOne({
where: { id: requestBody.position, posMasterId: requestBody.posMaster },
relations: ["posExecutive"],
});
if (_position) {
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
_profile.posMasterNo = getPosMasterNo(dataMaster);
_profile.org = getOrgFullName(dataMaster);
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if(!dataMaster.isSit){
if (!dataMaster.isSit) {
_profile.position = _position.positionName;
_profile.posTypeId = _position.posTypeId;
_profile.posLevelId = _position.posLevelId;
await this.profileRepository.save(_profile);
setLogDataDiff(request, { before, after: _profile });
_profile.positionField = _position.positionField ?? _null;
_profile.posExecutive = _position.posExecutive?.posExecutiveName ?? _null;
_profile.positionArea = _position.positionArea ?? _null;
_profile.positionExecutiveField = _position.positionExecutiveField ?? _null;
}
await this.profileRepository.save(_profile);
setLogDataDiff(request, { before, after: _profile });
}
}
dataMaster.current_holderId = requestBody.profileId;
@ -5169,9 +5202,9 @@ export class PositionController extends Controller {
}
/**
* API
* API
*
* @summary ORG_070 - (ADMIN) #56
* @summary
*
*/
@Post("master/position-condition")
@ -5182,7 +5215,7 @@ export class PositionController extends Controller {
id: string;
revisionId: string;
type: number;
isAll: boolean;
isAll: boolean; // true คือเลือกเฉพาะตำแหน่งติดเงื่อนไข / false คือเลือกตำแหน่งทั้งหมด
page: number;
pageSize: number;
keyword?: string;
@ -5202,7 +5235,7 @@ export class PositionController extends Controller {
let level: any = resolveNodeLevel(orgDna);
const cannotViewRootPosMaster =
(_data.privilege === "PARENT") ||
_data.privilege === "PARENT" ||
(_data.privilege === "BROTHER" && level > 1) ||
(_data.privilege === "CHILD" && level > 0) ||
(_data.privilege === "NORMAL" && level != 0);
@ -5234,46 +5267,46 @@ export class PositionController extends Controller {
typeCondition = {
...(cannotViewRootPosMaster ? { orgRootId: null } : { orgRootId: body.id }),
};
if (!body.isAll) {
checkChildConditions = {
orgChild1Id: IsNull(),
};
searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
} else {
}
// if (!body.isAll) {
// checkChildConditions = {
// orgChild1Id: IsNull(),
// };
// searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 1) {
typeCondition = {
...(cannotViewChild1PosMaster ? { orgChild1Id: null } : { orgChild1Id: body.id }),
};
if (!body.isAll) {
checkChildConditions = {
orgChild2Id: IsNull(),
};
searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
} else {
}
// if (!body.isAll) {
// checkChildConditions = {
// orgChild2Id: IsNull(),
// };
// searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 2) {
typeCondition = {
...(cannotViewChild2PosMaster ? { orgChild2Id: null } : { orgChild2Id: body.id }),
};
if (!body.isAll) {
checkChildConditions = {
orgChild3Id: IsNull(),
};
searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
} else {
}
// if (!body.isAll) {
// checkChildConditions = {
// orgChild3Id: IsNull(),
// };
// searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 3) {
typeCondition = {
...(cannotViewChild3PosMaster ? { orgChild3Id: null } : { orgChild3Id: body.id }),
};
if (!body.isAll) {
checkChildConditions = {
orgChild4Id: IsNull(),
};
searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
} else {
}
// if (!body.isAll) {
// checkChildConditions = {
// orgChild4Id: IsNull(),
// };
// searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 4) {
typeCondition = {
...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }),
@ -5346,7 +5379,7 @@ export class PositionController extends Controller {
(masterId.length > 0
? { id: In(masterId) }
: { posMasterNo: Like(`%${body.keyword}%`) })),
current_holderId: IsNull(),
...(!body.isAll && { isCondition: true }),
},
];
let [posMaster, total] = await AppDataSource.getRepository(PosMaster)
@ -5415,15 +5448,15 @@ export class PositionController extends Controller {
new Brackets((qb) => {
qb.andWhere(
body.keyword != null && body.keyword != ""
? body.isAll == false
? searchShortName
: `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'`
? `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'`
: "1=1",
)
.andWhere(checkChildConditions)
.andWhere(typeCondition)
.andWhere(revisionCondition)
.andWhere({ current_holderId: IsNull() });
.andWhere(revisionCondition);
if (!body.isAll) {
qb.andWhere({ isCondition: true });
}
}),
)
.orWhere(
@ -5433,8 +5466,10 @@ export class PositionController extends Controller {
)
.andWhere(checkChildConditions)
.andWhere(typeCondition)
.andWhere(revisionCondition)
.andWhere({ current_holderId: IsNull() });
.andWhere(revisionCondition);
if (!body.isAll) {
qb.andWhere({ isCondition: true });
}
}),
)
.orderBy("orgRoot.orgRootOrder", "ASC")

View file

@ -1679,35 +1679,84 @@ export class ProfileController extends Controller {
// ประวัติพ้นจากราชการ
let retires = [];
const currentDate = new Date();
// todo: รอข้อสรุป
// const retire_raw = await this.salaryRepo.findOne({
// where: {
// profileId: id,
// commandCode: In(["12", "15", "16"]),
// },
// order: { order: "desc" },
// });
// if (retire_raw) {
// const startDate = retire_raw.commandDateAffect;
// commandCode ที่ถือว่าออกจากราชการ
const retireCommandCodes = ["12", "15", "16"];
// // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน
// let daysCount = 0;
// if (startDate) {
// const start = new Date(startDate);
// daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
// }
// ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ
const salaries = await this.salaryRepo.find({
where: { profileId: id },
order: { order: "ASC" },
});
// const startDateStr = startDate
// ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate))
// : "-";
// มีคำสั่งพ้นราชการหรือไม่
if (
salaries.length > 0 &&
salaries.some((s) => s.commandCode && retireCommandCodes.includes(s.commandCode))
) {
// กรองข้อมูลซ้ำตาม commandDateAffect
const uniqueSalaries = salaries.filter(
(item, index, self) =>
index ===
self.findIndex(
(t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime(),
),
);
// retires.push({
// date: `${startDateStr}`,
// detail: retire_raw.commandName ?? "-",
// day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-"
// });
// }
// วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ"
for (let i = 0; i < uniqueSalaries.length; i++) {
const current = uniqueSalaries[i];
// เป็นคำสั่งออกจากราชการหรือไม่
if (current.commandCode && retireCommandCodes.includes(current.commandCode)) {
const startDate = current.commandDateAffect;
let endDate: Date | null = null;
let endRecord = null;
// หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ)
for (let j = i + 1; j < uniqueSalaries.length; j++) {
const next = uniqueSalaries[j];
if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) {
endDate = next.commandDateAffect;
endRecord = next;
break;
}
}
// ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน
if (!endDate) {
endDate = currentDate;
}
// คำนวณจำนวนวัน
let daysCount = 0;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
}
// สร้าง detail จาก commandName + remark
const commandName = current.commandName || "";
const remark = current.remark || "";
const detail = `${commandName} ${remark}`.trim();
// แปลงวันที่เป็น format ไทย
const startDateStr = startDate
? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate))
: "-";
const endDateStr = endDate
? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate))
: "-";
retires.push({
date: `${startDateStr} - ${endDateStr}`,
detail: detail || "-",
day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-",
});
}
}
}
// กรณีไม่มีข้อมูล
if (retires.length === 0) {
@ -11965,4 +12014,90 @@ export class ProfileController extends Controller {
return new HttpSuccess();
}
/**
* API keycloak
*
* @summary keycloak
*
*/
@Get("keycloak/position-checkin")
async getProfileByKeycloakForCheckin(@Request() request: { user: Record<string, any> }) {
const userSub = request.user.sub;
const relations = [
"current_holders",
"current_holders.orgRoot",
"current_holders.orgChild1",
"current_holders.orgChild2",
"current_holders.orgChild3",
"current_holders.orgChild4",
];
const [officerProfile, orgRevisionPublish] = await Promise.all([
this.profileRepo.findOne({
where: { keycloak: userSub },
relations,
}),
this.orgRevisionRepo.findOne({
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
}),
]);
if (!orgRevisionPublish) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบแบบร่างโครงสร้าง");
}
let profile: any = officerProfile;
let profileType: "OFFICER" | "EMPLOYEE" = "OFFICER";
if (!profile) {
profile = await this.profileEmpRepo.findOne({
where: { keycloak: userSub },
relations,
});
profileType = "EMPLOYEE";
}
if (!profile) {
if (request.user.role.includes("SUPER_ADMIN")) {
return new HttpSuccess(null);
}
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ");
}
const currentHolder =
profile.current_holders?.find((x: any) => x.orgRevisionId == orgRevisionPublish.id) ?? null;
const root = currentHolder?.orgRoot ?? null;
const child1 = currentHolder?.orgChild1 ?? null;
const child2 = currentHolder?.orgChild2 ?? null;
const child3 = currentHolder?.orgChild3 ?? null;
const child4 = currentHolder?.orgChild4 ?? null;
const _profile: any = {
profileId: profile.id,
keycloak: profile.keycloak,
prefix: profile.prefix,
avatar: profile.avatar,
profileType,
isProbation: profile.isProbation,
avatarName: profile.avatarName,
firstName: profile.firstName,
lastName: profile.lastName,
citizenId: profile.citizenId,
root: root?.orgRootName ?? null,
child1: child1?.orgChild1Name ?? null,
child2: child2?.orgChild2Name ?? null,
child3: child3?.orgChild3Name ?? null,
child4: child4?.orgChild4Name ?? null,
privacyCheckin: profile.privacyCheckin,
privacyUser: profile.privacyUser,
privacyMgt: profile.privacyMgt,
...(profileType !== "OFFICER" ? { type: profile.employeeClass } : {}),
};
return new HttpSuccess(_profile);
}
}

View file

@ -1950,35 +1950,78 @@ export class ProfileEmployeeController extends Controller {
// ประวัติพ้นจากราชการ
let retires = [];
const currentDate = new Date();
// todo: รอข้อสรุป
// const retire_raw = await this.salaryRepo.findOne({
// where: {
// profileEmployeeId: id,
// commandCode: In(["12", "15", "16"]),
// },
// order: { order: "desc" },
// });
// if (retire_raw) {
// const startDate = retire_raw.commandDateAffect;
// commandCode ที่ถือว่าออกจากราชการ
const retireCommandCodes = ["12", "15", "16"];
// // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน
// let daysCount = 0;
// if (startDate) {
// const start = new Date(startDate);
// daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
// }
// ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ
const salaries = await this.salaryRepo.find({
where: { profileEmployeeId: id },
order: { order: "ASC" },
});
// const startDateStr = startDate
// ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate))
// : "-";
// มีคำสั่งพ้นราชการหรือไม่
if (salaries.length > 0 && salaries.some((s) => s.commandCode &&
retireCommandCodes.includes(s.commandCode))) {
// กรองข้อมูลซ้ำตาม commandDateAffect
const uniqueSalaries = salaries.filter((item, index, self) =>
index === self.findIndex((t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime())
);
// retires.push({
// date: `${startDateStr} - ปัจจุบัน`,
// detail: retire_raw.commandName ?? "-",
// day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-"
// });
// }
// วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ"
for (let i = 0; i < uniqueSalaries.length; i++) {
const current = uniqueSalaries[i];
// เป็นคำสั่งออกจากราชการหรือไม่
if (current.commandCode && retireCommandCodes.includes(current.commandCode)) {
const startDate = current.commandDateAffect;
let endDate: Date | null = null;
let endRecord = null;
// หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ)
for (let j = i + 1; j < uniqueSalaries.length; j++) {
const next = uniqueSalaries[j];
if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) {
endDate = next.commandDateAffect;
endRecord = next;
break;
}
}
// ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน
if (!endDate) {
endDate = currentDate;
}
// คำนวณจำนวนวัน
let daysCount = 0;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
}
// สร้าง detail จาก commandName + remark
const commandName = current.commandName || "";
const remark = current.remark || "";
const detail = `${commandName} ${remark}`.trim();
// แปลงวันที่เป็น format ไทย
const startDateStr = startDate
? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate))
: "-";
const endDateStr = endDate
? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate))
: "-";
retires.push({
date: `${startDateStr} - ${endDateStr}`,
detail: detail || "-",
day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-"
});
}
}
}
// กรณีไม่มีข้อมูล
if (retires.length === 0) {

View file

@ -6,8 +6,6 @@ import HttpError from "../interfaces/http-error";
import { RequestWithUser } from "../middlewares/user";
import { Profile } from "../entities/Profile";
import { ProfileGovernment, UpdateProfileGovernment } from "../entities/ProfileGovernment";
import { Position } from "../entities/Position";
import { PosMaster } from "../entities/PosMaster";
import {
calculateAge,
calculateGovAge,
@ -15,7 +13,6 @@ import {
setLogDataDiff,
} from "../interfaces/utils";
import permission from "../interfaces/permission";
import { OrgRevision } from "../entities/OrgRevision";
import { In } from "typeorm";
@Route("api/v1/org/profile/government")
@Tags("ProfileGovernment")
@ -23,9 +20,6 @@ import { In } from "typeorm";
export class ProfileGovernmentHistoryController extends Controller {
private profileRepo = AppDataSource.getRepository(Profile);
private govRepo = AppDataSource.getRepository(ProfileGovernment);
private positionRepo = AppDataSource.getRepository(Position);
private posMasterRepo = AppDataSource.getRepository(PosMaster);
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
/**
*
* @summary
@ -33,13 +27,6 @@ export class ProfileGovernmentHistoryController extends Controller {
*/
@Get("user")
public async getGovHistoryUser(@Request() request: { user: Record<string, any> }) {
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
@ -51,79 +38,19 @@ export class ProfileGovernmentHistoryController extends Controller {
posLevel: true,
},
});
const posMaster = await this.posMasterRepo.findOne({
where: {
// orgRevision: {
// orgRevisionIsCurrent: true,
// orgRevisionIsDraft: false,
// },
orgRevisionId: orgRevision?.id,
current_holderId: profile.id,
},
order: { createdAt: "DESC" },
relations: {
orgRoot: true,
orgChild1: true,
orgChild2: true,
orgChild3: true,
orgChild4: true,
},
});
const position = await this.positionRepo.findOne({
where: {
positionIsSelected: true,
posMaster: {
// orgRevision: {
// orgRevisionIsCurrent: true,
// orgRevisionIsDraft: false,
// },
orgRevisionId: orgRevision?.id,
current_holderId: profile.id,
},
},
order: { createdAt: "DESC" },
relations: {
posExecutive: true,
},
});
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const fullNameParts = [
posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name,
posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name,
posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name,
posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name,
posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName,
];
const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n");
let orgShortName = "";
if (posMaster != null) {
if (posMaster.orgChild1Id === null) {
orgShortName = posMaster.orgRoot?.orgRootShortName;
} else if (posMaster.orgChild2Id === null) {
orgShortName = posMaster.orgChild1?.orgChild1ShortName;
} else if (posMaster.orgChild3Id === null) {
orgShortName = posMaster.orgChild2?.orgChild2ShortName;
} else if (posMaster.orgChild4Id === null) {
orgShortName = posMaster.orgChild3?.orgChild3ShortName;
} else {
orgShortName = posMaster.orgChild4?.orgChild4ShortName;
}
}
//posMaster?.isSit แก้ไขชั่วคราว
// ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว
const data = {
org: org, //สังกัด
positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน
org: record.org ?? null, //สังกัด
positionField: record.positionField ?? null, //สายงาน
position: record.position, //ตำแหน่ง
posLevel: record.posLevel == null ? null : record.posLevel.posLevelName, //ระดับ
posMasterNo: posMaster == null ? null : `${orgShortName} ${posMaster.posMasterNo}`, //เลขที่ตำแหน่ง
posMasterNo: record.posMasterNo ?? null, //เลขที่ตำแหน่ง
posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท
posExecutive:
position == null || position.posExecutive == null || posMaster?.isSit
? null
: position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร
positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา
positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร
posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร
positionArea: record.positionArea ?? null, //ด้าน/สาขา
positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร
dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate),
dateRetireLaw: record.dateRetireLaw ?? null,
// govAge: record.dateStart == null ? null : calculateAge(record.dateStart),
@ -135,10 +62,10 @@ export class ProfileGovernmentHistoryController extends Controller {
govAgePlus: record.govAgePlus,
reasonSameDate: record.reasonSameDate,
};
return new HttpSuccess(data);
}
/**
*
* @summary
@ -150,25 +77,17 @@ export class ProfileGovernmentHistoryController extends Controller {
let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_OFFICER");
if (_workflow == false)
await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId);
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
// ค้นหา profile ก่อน
const record = await this.profileRepo.findOne({
where: { id: profileId },
relations: ["posType", "posLevel"],
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile");
}
// ค้นหา profileSalary แยกต่างหาก
const profileWithSalary = await this.profileRepo.findOne({
where: {
@ -201,70 +120,13 @@ export class ProfileGovernmentHistoryController extends Controller {
},
},
});
// ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ
record.profileSalary = profileWithSalary?.profileSalary || [];
const posMaster = await this.posMasterRepo.findOne({
where: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
order: { createdAt: "DESC" },
relations: {
orgRoot: true,
orgChild1: true,
orgChild2: true,
orgChild3: true,
orgChild4: true,
},
});
const position = await this.positionRepo.findOne({
where: {
positionIsSelected: true,
posMaster: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
},
order: { createdAt: "DESC" },
relations: {
posExecutive: true,
},
});
// if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const fullNameParts = [
posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name,
posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name,
posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name,
posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name,
posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName,
];
const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n");
let orgShortName = "";
if (posMaster != null) {
if (posMaster.orgChild1Id === null) {
orgShortName = posMaster.orgRoot?.orgRootShortName ?? "";
} else if (posMaster.orgChild2Id === null) {
orgShortName = posMaster.orgChild1?.orgChild1ShortName ?? "";
} else if (posMaster.orgChild3Id === null) {
orgShortName = posMaster.orgChild2?.orgChild2ShortName ?? "";
} else if (posMaster.orgChild4Id === null) {
orgShortName = posMaster.orgChild3?.orgChild3ShortName ?? "";
} else {
orgShortName = posMaster.orgChild4?.orgChild4ShortName ?? "";
}
}
let _OrgLeave: any = [];
let _profileSalary: any = null;
if (record?.isLeave && record?.profileSalary.length > 0) {
// _OrgLeave = [
// record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null,
// record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null,
// record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null,
// record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null,
// record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null,
// ];
if (record.leaveType == "RETIRE") {
_profileSalary =
record?.profileSalary.length > 1
@ -288,27 +150,23 @@ export class ProfileGovernmentHistoryController extends Controller {
}
}
const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n");
//posMaster?.isSit แก้ไขชั่วคราว
// ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว
const data = {
org: record?.isLeave == false ? org : orgLeave, //สังกัด
positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน
org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด
positionField: record.positionField ?? null, //สายงาน
position: record?.position, //ตำแหน่ง
posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ
posMasterNo:
record?.isLeave == false
? posMaster == null
? null
: `${orgShortName} ${posMaster.posMasterNo}`
? record.posMasterNo ?? null
: _profileSalary != null
? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}`
: null, //เลขที่ตำแหน่ง
posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท
posExecutive:
position == null || position.posExecutive == null || posMaster?.isSit
? null
: position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร
positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา
positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร
posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร
positionArea: record.positionArea ?? null, //ด้าน/สาขา
positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร
dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate),
dateRetireLaw: record?.dateRetireLaw ?? null,
// govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart),
@ -320,30 +178,22 @@ export class ProfileGovernmentHistoryController extends Controller {
govAgePlus: record?.govAgePlus,
reasonSameDate: record?.reasonSameDate,
};
return new HttpSuccess(data);
}
@Get("admin/{profileId}")
public async getGovHistoryAdmin(@Path() profileId: string) {
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
// ค้นหา profile ก่อน
const record = await this.profileRepo.findOne({
where: { id: profileId },
relations: ["posType", "posLevel"],
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile");
}
// ค้นหา profileSalary แยกต่างหาก
const profileWithSalary = await this.profileRepo.findOne({
where: {
@ -376,70 +226,13 @@ export class ProfileGovernmentHistoryController extends Controller {
},
},
});
// ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ
record.profileSalary = profileWithSalary?.profileSalary || [];
const posMaster = await this.posMasterRepo.findOne({
where: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
order: { createdAt: "DESC" },
relations: {
orgRoot: true,
orgChild1: true,
orgChild2: true,
orgChild3: true,
orgChild4: true,
},
});
const position = await this.positionRepo.findOne({
where: {
positionIsSelected: true,
posMaster: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
},
order: { createdAt: "DESC" },
relations: {
posExecutive: true,
},
});
// if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const fullNameParts = [
posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name,
posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name,
posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name,
posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name,
posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName,
];
const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n");
let orgShortName = "";
if (posMaster != null) {
if (posMaster.orgChild1Id === null) {
orgShortName = posMaster.orgRoot?.orgRootShortName;
} else if (posMaster.orgChild2Id === null) {
orgShortName = posMaster.orgChild1?.orgChild1ShortName;
} else if (posMaster.orgChild3Id === null) {
orgShortName = posMaster.orgChild2?.orgChild2ShortName;
} else if (posMaster.orgChild4Id === null) {
orgShortName = posMaster.orgChild3?.orgChild3ShortName;
} else {
orgShortName = posMaster.orgChild4?.orgChild4ShortName;
}
}
let _OrgLeave: any = [];
let _profileSalary: any = null;
if (record?.isLeave && record?.profileSalary.length > 0) {
// _OrgLeave = [
// record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null,
// record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null,
// record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null,
// record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null,
// record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null,
// ];
if (record.leaveType == "RETIRE") {
_profileSalary =
record?.profileSalary.length > 1
@ -463,27 +256,23 @@ export class ProfileGovernmentHistoryController extends Controller {
}
}
const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n");
//posMaster?.isSit แก้ไขชั่วคราว
// ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว
const data = {
org: record?.isLeave == false ? org : orgLeave, //สังกัด
positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน
org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด
positionField: record.positionField ?? null, //สายงาน
position: record?.position, //ตำแหน่ง
posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ
posMasterNo:
record?.isLeave == false
? posMaster == null
? null
: `${orgShortName} ${posMaster.posMasterNo}`
? record.posMasterNo ?? null
: _profileSalary != null
? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}`
: null, //เลขที่ตำแหน่ง
posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท
posExecutive:
position == null || position.posExecutive == null || posMaster?.isSit
? null
: position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร
positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา
positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร
posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร
positionArea: record.positionArea ?? null, //ด้าน/สาขา
positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร
dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate),
dateRetireLaw: record?.dateRetireLaw ?? null,
// govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart),
@ -496,10 +285,10 @@ export class ProfileGovernmentHistoryController extends Controller {
reasonSameDate: record?.reasonSameDate,
isLeave: record?.isLeave,
};
return new HttpSuccess(data);
}
/**
*
* @summary by keycloak
@ -517,7 +306,7 @@ export class ProfileGovernmentHistoryController extends Controller {
});
return new HttpSuccess(record);
}
/**
*
* @summary
@ -533,12 +322,12 @@ export class ProfileGovernmentHistoryController extends Controller {
order: { lastUpdatedAt: "DESC" },
where: { profileId: profileId },
});
// record.pop();
return new HttpSuccess(record);
}
/**
*
* @summary
@ -554,14 +343,14 @@ export class ProfileGovernmentHistoryController extends Controller {
const record = await this.profileRepo.findOne({
where: { id: profileId },
});
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const before = structuredClone(record);
const history = new ProfileGovernment();
Object.assign(record, body);
Object.assign(history, { ...record, id: undefined });
history.profileId = profileId;
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
@ -572,13 +361,14 @@ export class ProfileGovernmentHistoryController extends Controller {
history.createdFullName = req.user.name;
history.createdAt = new Date();
history.lastUpdatedAt = new Date();
await Promise.all([
this.profileRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.govRepo.save(history, { data: req }),
]);
return new HttpSuccess();
}
}

View file

@ -23,6 +23,7 @@ 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";
@ -65,10 +66,12 @@ export class ProfileSalaryController extends Controller {
await this.positionOfficerRepo.clear();
const profile = await this.profileRepo.find();
const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today");
let _currentDate = CURRENT_DATE[0].today;
const baseCurrentDate = CURRENT_DATE[0].today;
for await (const x of profile) {
if (x.isLeave) {
_currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate;
// 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 AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [
x.id,
@ -92,21 +95,18 @@ export class ProfileSalaryController extends Controller {
},
{ 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: (calDayDiff.days_diff / 365.2524).toFixed(4),
// Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4),
// Days: (calDayDiff.days_diff % 30.4375).toFixed(4),
Years: Math.floor(calDayDiff.days_diff / 365.2524),
Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12),
Days: Math.floor(calDayDiff.days_diff % 30.4375),
Years: year,
Months: month,
Days: day,
};
// data.push(_mapData);
await this.positionOfficerRepo.save(mapData);
data.push(mapData);
}
// await this.positionOfficerRepo.save(data);
await this.positionOfficerRepo.save(data);
return new HttpSuccess();
}
@ -115,11 +115,13 @@ export class ProfileSalaryController extends Controller {
let data: any = [];
await this.positionEmployeeRepo.clear();
const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today");
let _currentDate = CURRENT_DATE[0].today;
const baseCurrentDate = CURRENT_DATE[0].today;
const profile = await this.profileEmployeeRepo.find();
for await (const x of profile) {
if (x?.isLeave) {
_currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate;
// 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 AppDataSource.query("CALL GetProfileEmployeeSalaryPosition(?, ?)", [
x.id,
@ -143,21 +145,18 @@ export class ProfileSalaryController extends Controller {
},
{ 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: (calDayDiff.days_diff / 365.2524).toFixed(4),
// Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4),
// Days: (calDayDiff.days_diff % 30.4375).toFixed(4),
Years: Math.floor(calDayDiff.days_diff / 365.2524),
Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12),
Days: Math.floor(calDayDiff.days_diff % 30.4375),
Years: year,
Months: month,
Days: day,
};
// data.push(_mapData);
await this.positionEmployeeRepo.save(mapData);
data.push(mapData);
}
// await this.positionEmployeeRepo.save(data);
await this.positionEmployeeRepo.save(data);
return new HttpSuccess();
}
@ -167,10 +166,12 @@ export class ProfileSalaryController extends Controller {
await this.levelOfficerRepo.clear();
const profile = await this.profileRepo.find({ relations: ["posLevel", "posType"] });
const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today");
let _currentDate = CURRENT_DATE[0].today;
const baseCurrentDate = CURRENT_DATE[0].today;
for await (const x of profile) {
if (x?.isLeave) {
_currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate;
// 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 positionLevel = await AppDataSource.query("CALL GetProfileSalaryLevel(?, ?)", [
x.id,
@ -202,20 +203,20 @@ export class ProfileSalaryController extends Controller {
},
{ days_diff: 0, positionType: null, positionLevel: null, positionCee: null },
);
const { year, month, day } = calculateTenure(calDayDiff.days_diff);
const mapData: any = {
profileId: x.id,
positionType: calDayDiff.positionType,
positionLevel: calDayDiff.positionLevel,
positionCee: calDayDiff.positionCee,
days_diff: calDayDiff.days_diff,
Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4),
Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4),
Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4),
Years: x.posLevel == null ? 0 : year.toFixed(4),
Months: x.posLevel == null ? 0 : month.toFixed(4),
Days: x.posLevel == null ? 0 : day.toFixed(4),
};
// data.push(_mapData);
await this.levelOfficerRepo.save(mapData);
data.push(mapData);
}
// await this.levelOfficerRepo.save(data);
await this.levelOfficerRepo.save(data);
return new HttpSuccess();
}
@ -225,10 +226,12 @@ export class ProfileSalaryController extends Controller {
await this.levelEmployeeRepo.clear();
const profile = await this.profileEmployeeRepo.find({ relations: ["posLevel", "posType"] });
const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today");
let _currentDate = CURRENT_DATE[0].today;
const baseCurrentDate = CURRENT_DATE[0].today;
for await (const x of profile) {
if (x?.isLeave) {
_currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate;
// 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 positionLevel = await AppDataSource.query("CALL GetProfileEmployeeSalaryLevel(?, ?)", [
x.id,
@ -260,26 +263,27 @@ export class ProfileSalaryController extends Controller {
},
{ days_diff: 0, positionType: null, positionLevel: null, positionCee: null },
);
const { year, month, day } = calculateTenure(calDayDiff.days_diff);
const mapData: any = {
profileEmployeeId: x.id,
positionType: calDayDiff.positionType,
positionLevel: calDayDiff.positionLevel,
positionCee: calDayDiff.positionCee,
days_diff: calDayDiff.days_diff,
Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4),
Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4),
Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4),
Years: x.posLevel == null ? 0 : year.toFixed(4),
Months: x.posLevel == null ? 0 : month.toFixed(4),
Days: x.posLevel == null ? 0 : day.toFixed(4),
};
// data.push(_mapData);
await this.levelEmployeeRepo.save(mapData);
data.push(mapData);
}
// await this.levelEmployeeRepo.save(data);
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({
@ -290,10 +294,12 @@ export class ProfileSalaryController extends Controller {
},
});
const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today");
let _currentDate = CURRENT_DATE[0].today;
const baseCurrentDate = CURRENT_DATE[0].today;
for await (const x of profile) {
if (x?.isLeave) {
_currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate;
// 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: {
@ -331,16 +337,18 @@ export class ProfileSalaryController extends Controller {
},
{ days_diff: 0, positionExecutive: null },
);
const { year, month, day } = calculateTenure(calDayDiff.days_diff);
const mapData: any = {
profileId: x.id,
positionExecutiveName: calDayDiff.positionExecutive,
days_diff: calDayDiff.days_diff,
Years: (calDayDiff.days_diff / 365.2524).toFixed(4),
Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4),
Days: (calDayDiff.days_diff % 30.4375).toFixed(4),
Years: year.toFixed(4),
Months: month.toFixed(4),
Days: day.toFixed(4),
};
await this.positionExecutiveOfficerRepo.save(mapData);
data.push(mapData);
}
await this.positionExecutiveOfficerRepo.save(data);
return new HttpSuccess();
}
@ -602,10 +610,10 @@ export class ProfileSalaryController extends Controller {
acc.push(existing);
}
// Recalculate year, month, and day
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -641,10 +649,10 @@ export class ProfileSalaryController extends Controller {
acc.push(existing);
}
// Recalculate year, month, and day
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -675,10 +683,10 @@ export class ProfileSalaryController extends Controller {
acc.push(existing);
}
// Recalculate year, month, and day
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -739,10 +747,10 @@ export class ProfileSalaryController extends Controller {
acc.push(existing);
}
// Recalculate year, month, and day
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -782,10 +790,10 @@ export class ProfileSalaryController extends Controller {
acc.push(existing);
}
// Recalculate year, month, and day
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -819,10 +827,10 @@ export class ProfileSalaryController extends Controller {
acc.push(existing);
}
// Recalculate year, month, and day
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -911,6 +919,17 @@ export class ProfileSalaryController extends Controller {
else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง";
}
Object.assign(data, { ...body, ...meta });
// 12,15,16 isGovernment = false & dateGovernment = commandDateAffect
if (["12", "15", "16"].includes(body.commandCode ?? "")) {
data.isGovernment = false;
if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
data.isGovernment = true;
if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
}
const history = new ProfileSalaryHistory();
Object.assign(history, { ...data, id: undefined });
await this.salaryRepo.save(data, { data: req });
@ -1035,6 +1054,17 @@ export class ProfileSalaryController extends Controller {
else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง";
}
Object.assign(record, body);
// 12,15,16 isGovernment = false & dateGovernment = commandDateAffect
if (["12", "15", "16"].includes(body.commandCode ?? "")) {
record.isGovernment = false;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
record.isGovernment = true;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect;
}
Object.assign(history, { ...record, id: undefined });
history.profileSalaryId = salaryId;

View file

@ -27,6 +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 { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot";
import Extension from "../interfaces/extension";
@ -175,9 +176,10 @@ export class ProfileSalaryEmployeeController extends Controller {
acc.push(existing);
}
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -211,9 +213,10 @@ export class ProfileSalaryEmployeeController extends Controller {
acc.push(existing);
}
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -266,9 +269,10 @@ export class ProfileSalaryEmployeeController extends Controller {
acc.push(existing);
}
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -302,9 +306,10 @@ export class ProfileSalaryEmployeeController extends Controller {
acc.push(existing);
}
existing.year = Math.floor(existing.days / 365.2524);
existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.day = Math.ceil(existing.days % 30.4375);
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
return acc;
},
@ -398,6 +403,17 @@ export class ProfileSalaryEmployeeController extends Controller {
else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง";
}
Object.assign(data, { ...body, ...meta });
// 12,15,16 isGovernment = false & dateGovernment = commandDateAffect
if (["12", "15", "16"].includes(body.commandCode ?? "")) {
data.isGovernment = false;
if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
data.isGovernment = true;
if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
}
const history = new ProfileSalaryHistory();
Object.assign(history, { ...data, id: undefined });
const _null: any = null;
@ -532,6 +548,16 @@ export class ProfileSalaryEmployeeController extends Controller {
else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง";
}
Object.assign(record, body);
// 12,15,16 isGovernment = false & dateGovernment = commandDateAffect
if (["12", "15", "16"].includes(body.commandCode ?? "")) {
record.isGovernment = false;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
record.isGovernment = true;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null;
}
Object.assign(history, { ...record, id: undefined });
history.profileSalaryId = salaryId;

View file

@ -133,8 +133,8 @@ export class ProfileSalaryTempController extends Controller {
_data.child1 != undefined && _data.child1 != null
? _data.child1[0] != null
? `current_holders.orgChild1Id IN (:...child1)`
// : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
: `current_holders.orgChild1Id is null`
: // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
`current_holders.orgChild1Id is null`
: "1=1",
{
child1: _data.child1,
@ -545,8 +545,8 @@ export class ProfileSalaryTempController extends Controller {
_data.child1 != undefined && _data.child1 != null
? _data.child1[0] != null
? `current_holders.orgChild1Id IN (:...child1)`
// : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
: `current_holders.orgChild1Id is null`
: // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
`current_holders.orgChild1Id is null`
: "1=1",
{
child1: _data.child1,
@ -1233,6 +1233,13 @@ export class ProfileSalaryTempController extends Controller {
isDelete: false,
};
Object.assign(data, { ...body, ...meta });
// if (["12", "15", "16"].includes(body.commandCode ?? "")) {
// data.isGovernment = false;
// if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
// } else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
// data.isGovernment = true;
// if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
// }
await this.salaryRepo.save(data, { data: req });
setLogDataDiff(req, { before, after: data });
@ -1433,10 +1440,10 @@ export class ProfileSalaryTempController extends Controller {
profileEmployeeId: x.profileEmployeeId,
dateStart: x.commandDateAffect,
dateEnd: null,
posNo: `${x.posNoAbb} ${x.posNo}`,
posNo: `${x.posNoAbb ?? ""} ${x.posNo ?? ""}`.trim(),
position: x.positionName,
commandId: x.commandId,
refCommandNo: `${x.commandNo}/${x.commandYear}`,
refCommandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined,
refCommandDate: x.commandDateAffect,
status: false,
isDeleted: false,
@ -1456,7 +1463,7 @@ export class ProfileSalaryTempController extends Controller {
dateStart: x.commandDateAffect,
dateEnd: null,
commandId: x.commandId,
commandNo: `${x.commandNo}/${x.commandYear}`,
commandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined,
commandName: x.commandName ?? "ให้ช่วยราชการ",
refCommandDate: x.commandDateSign,
refId: x.refId,
@ -1509,6 +1516,16 @@ export class ProfileSalaryTempController extends Controller {
const before = structuredClone(record);
Object.assign(record, body);
// 12,15,16 isGovernment = false & dateGovernment = commandDateAffect
if (["12", "15", "16"].includes(body.commandCode ?? "")) {
record.isGovernment = false;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
record.isGovernment = true;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect;
}
record.isEdit = true;
record.lastUpdateUserId = req.user.sub;

View file

@ -814,6 +814,68 @@ export class KeycloakController extends Controller {
if (!result) throw new Error("Failed. Cannot remove group to user.");
}
@Post("user/reset-password")
@Security("bearerAuth", ["admin"])
async resetUserPassword(@Request() req: RequestWithUser, @Body() body: { keycloak: string }) {
if (!req.user.role.includes("ADMIN") && !req.user.role.includes("SUPER_ADMIN")) {
throw new HttpError(HttpStatus.FORBIDDEN, "ไม่มีสิทธิ์ดำเนินการ");
}
let profile: Profile | ProfileEmployee | null = await this.profileRepo.findOne({
where: { keycloak: body.keycloak },
select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"],
});
let isEmployee = false;
if (!profile) {
profile = await this.profileEmpRepo.findOne({
where: { keycloak: body.keycloak, employeeClass: "PERM" },
select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"],
});
isEmployee = true;
}
if (!profile) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้");
}
if (!profile.keycloak) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ผู้ใช้ไม่ได้เชื่อมต่อกับ Keycloak");
}
let newPassword: string;
const isProduction = process.env.NODE_ENV === "production";
if (isProduction && profile.birthDate) {
const _date = new Date(profile.birthDate.toDateString())
.getDate()
.toString()
.padStart(2, "0");
const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1)
.toString()
.padStart(2, "0");
const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543;
newPassword = `${_date}${_month}${_year}`;
} else {
newPassword = "P@ssw0rd";
}
const result = await changeUserPassword(profile.keycloak, newPassword);
if (!result) {
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "ไม่สามารถรีเซ็ตรหัสผ่านได้");
}
addLogSequence(req, {
action: "reset-password",
status: "success",
description: `รีเซ็ตรหัสผ่านสำหรับ ${profile.firstName} ${profile.lastName} (${profile.citizenId})`,
});
const response = new HttpSuccess();
response.message = "รีเซ็ตรหัสผ่านสำเร็จ";
return response;
}
@Get("user/role/{id}")
async getRoleUser(@Request() req: RequestWithUser, @Path("id") id: string) {
const profile = await this.profileRepo.findOne({

View file

@ -237,11 +237,21 @@ export class WorkflowController extends Controller {
savedStates.find((state) => state.id === so.stateId && state.order === 1),
);
// add link sysName = REGISTRY_PROFILE or REGISTRY_PROFILE_EMP
let notiLink = "";
if (body.sysName === "REGISTRY_PROFILE") {
notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`;
} else if (body.sysName === "REGISTRY_PROFILE_EMP") {
notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`;
} else if (body.sysName === "REGISTRY_IDP") {
notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`;
}
const notificationReceivers = stateOperatorUsersToCreate
.filter((user) => firstStateOperators.some((op) => op.operator === user.operator))
.map((user) => ({
receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId,
notiLink: "",
notiLink: notiLink,
}));
// ส่ง notification แบบ fire-and-forget
@ -904,14 +914,14 @@ export class WorkflowController extends Controller {
const roodIds = [posMasterUser.orgRootId];
const orgRoot = await this.orgRootRepo.findOne({
select: { id: true, isDeputy: true },
where: {
where: {
id: Not(posMasterUser.orgRootId),
isDeputy: true,
orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
},
});
if (orgRoot && orgRoot.isDeputy) {
roodIds.push(orgRoot.id)
roodIds.push(orgRoot.id);
}
// 2. Pre-calculate conditions - ย้ายออกมาข้างนอก

View file

@ -140,6 +140,54 @@ export class Profile extends EntityBase {
})
posTypeId: string | null;
@Column({
nullable: true,
comment: "สายงาน",
length: 45,
default: null,
})
positionField: string;
@Column({
nullable: true,
comment: "ตำแหน่งทางการบริหาร",
length: 255,
default: null,
})
posExecutive?: string;
@Column({
nullable: true,
comment: "ด้าน/สาขา",
length: 255,
default: null,
})
positionArea?: string;
@Column({
nullable: true,
comment: "ด้านทางการบริหาร",
length: 255,
default: null,
})
positionExecutiveField?: string;
@Column({
nullable: true,
comment: "เลขที่ตำแหน่ง",
length: 255,
default: null,
})
posMasterNo?: string;
@Column({
nullable: true,
comment: "สังกัด",
type: "text",
default: null,
})
org?: string;
@Column({
nullable: true,
length: 255,

View file

@ -116,6 +116,34 @@ export async function withRetry<T>(
throw lastError;
}
/**
* Fetch with timeout
* Aborts request if it takes longer than specified timeout
*/
async function fetchWithTimeout(
url: RequestInfo | URL,
options: RequestInit = {},
timeout: number = 10000,
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error: any) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
}
const KC_URL = process.env.KC_URL;
const KC_REALMS = process.env.KC_REALMS;
const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID;
@ -144,10 +172,12 @@ export function isTokenExpired(token: string, beforeExpire: number = 30) {
/**
* Get token from keycloak if needed
* Returns null if Keycloak is unavailable
*/
export async function getToken() {
export async function getToken(): Promise<string | null> {
if (!KC_CLIENT_ID || !KC_SECRET) {
throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
console.error("[getToken] KC_CLIENT_ID and KC_SECRET are required");
return null;
}
if (token && !isTokenExpired(token)) return token;
@ -158,22 +188,35 @@ export async function getToken() {
body.append("client_secret", KC_SECRET);
body.append("grant_type", "client_credentials");
const res = await fetch(`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, {
method: "POST",
body: body,
}).catch((e) => console.error(e));
try {
const res = await fetchWithTimeout(
`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`,
{
method: "POST",
body: body,
},
10000,
);
if (!res) {
throw new Error("Cannot get token from keycloak.");
if (!res.ok) {
console.error(`[getToken] Keycloak token request failed: ${res.status}`);
return null;
}
const data = (await res.json()) as any;
if (data && data.access_token) {
token = data.access_token;
console.log(`[getToken] Token refreshed successfully`);
return token;
}
console.error("[getToken] No access_token in response");
return null;
} catch (error: any) {
console.error(`[getToken] Failed to get token: ${error.message}`);
return null;
}
const data = (await res.json()) as any;
if (data && data.access_token) {
token = data.access_token;
}
console.log(`token: ${token}`);
return token;
}
/**
@ -189,10 +232,16 @@ export async function createUser(
opts?: Record<string, any>,
token?: string,
) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[createUser] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
method: "POST",
@ -206,7 +255,6 @@ export async function createUser(
if (!res) return false;
if (!res.ok) {
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
return await res.json();
}
@ -223,10 +271,16 @@ export async function createUser(
* @returns user if success, false otherwise.
*/
export async function getUser(userId: string, token?: string) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[getUser] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
}).catch((e) => console.log("Keycloak Error: ", e));
@ -245,10 +299,16 @@ export async function getUser(userId: string, token?: string) {
* @returns user if success, false otherwise.
*/
export async function getUserByUsername(citizenId: string, token?: string) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[getUserByUsername] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
}).catch((e) => console.log("Keycloak Error: ", e));
@ -379,23 +439,38 @@ export async function getUserCountOrg(first = "", max = "", search = "", userIds
export async function editUser(userId: string, opts: Record<string, any>) {
const { password, ...rest } = opts;
const token = await getToken();
if (!token) {
console.error("[editUser] Failed to get Keycloak token");
return false;
}
// Get existing user data to preserve other fields
const existingUser = await getUser(userId, token);
if (!existingUser) {
console.error(`[editUser] User ${userId} not found in Keycloak`);
return false;
}
// Merge existing user data with updated fields
const updatedUser = {
...existingUser,
...rest,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
};
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"authorization": `Bearer ${token}`,
"content-type": `application/json`,
},
method: "PUT",
body: JSON.stringify({
enabled: true,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
...rest,
}),
body: JSON.stringify(updatedUser),
}).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false;
if (!res.ok) {
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
return await res.json();
}
@ -419,6 +494,24 @@ export async function updateName(
) {
// const { password, ...rest } = opts;
// Get existing user data to preserve other fields
const existingUser = await getUser(userId);
if (!existingUser) {
console.error(`[updateName] User ${userId} not found in Keycloak`);
return false;
}
// Merge existing user data with updated name fields
const updatedUser = {
...existingUser,
firstName,
lastName,
attributes: {
...(existingUser.attributes || {}),
prefix,
},
};
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore
headers: {
@ -426,16 +519,7 @@ export async function updateName(
"content-type": `application/json`,
},
method: "PUT",
body: JSON.stringify({
enabled: true,
// credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
// ...rest,
firstName,
lastName,
attributes: {
prefix,
},
}),
body: JSON.stringify(updatedUser),
}).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false;
@ -486,10 +570,16 @@ export async function enableStatus(userId: string, status: boolean) {
* @returns user true if success, false otherwise.
*/
export async function deleteUser(userId: string, token?: string) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[deleteUser] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
method: "DELETE",
@ -871,10 +961,16 @@ export async function removeUserGroup(userId: string, groupId: string) {
// Function to change user password
export async function changeUserPassword(userId: string, newPassword: string) {
try {
const token = await getToken();
if (!token) {
console.error("[changeUserPassword] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"authorization": `Bearer ${token}`,
"content-type": `application/json`,
},
method: "PUT",
@ -885,6 +981,15 @@ export async function changeUserPassword(userId: string, newPassword: string) {
}),
}).catch((e) => console.log("Keycloak Error: ", e));
if (!res) {
console.error("[changeUserPassword] No response from Keycloak");
return false;
}
if (!res.ok) {
console.error(`[changeUserPassword] Failed to change password: ${res.status}`);
return false;
}
return true;
} catch (error) {
console.error("Error changing password:", error);
@ -895,60 +1000,61 @@ export async function changeUserPassword(userId: string, newPassword: string) {
// Function to reset password
export async function resetPassword(username: string) {
try {
// if (!API_KEY || !AUTH_ACCOUNT_SECRET) {
// throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
// }
// const body = new URLSearchParams();
// body.append("client_id", "gettoken");
// body.append("client_secret", AUTH_ACCOUNT_SECRET?.toString());
// body.append("grant_type", "client_credentials");
// const tokenResponse = await fetch(`${process.env.KC_URL}/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, {
// method: "POST",
// headers: {
// "Content-Type": "application/x-www-form-urlencoded",
// api_key: API_KEY,
// },
// body: body
// });
// if (!tokenResponse.ok) {
// throw new Error("Failed to get admin token");
// }
// const tokenData = await tokenResponse.json();
// const adminToken = tokenData.access_token;
const token = await getToken();
if (!token) {
console.error("[resetPassword] Failed to get Keycloak token");
return false;
}
const users = await fetch(
const users = await fetchWithTimeout(
`${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`,
{
headers: {
authorization: `Bearer ${await getToken()}`,
// "authorization": `Bearer ${adminToken}`,
authorization: `Bearer ${token}`,
"content-type": `application/json`,
},
},
10000,
);
if (!users.ok) {
const errorText = await users.text();
console.error(`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`);
return false;
}
const usersData = await users.json();
if (!usersData || usersData.length === 0) {
console.error(`[resetPassword] User not found with email: ${username}`);
return false;
}
const userId = usersData[0].id;
const resetResponse = await fetch(
const resetResponse = await fetchWithTimeout(
`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${await getToken()}`,
// "Authorization": `Bearer ${adminToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(["UPDATE_PASSWORD"]),
},
10000,
);
if (!resetResponse.ok) {
const errorText = await resetResponse.text();
console.error(`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`);
return false;
}
console.log(`[resetPassword] Password reset email sent successfully to: ${username}`);
return { message: "Password reset email sent" };
} catch (error) {
console.error("Error triggering password reset:", error);
} catch (error: any) {
console.error(`[resetPassword] Error triggering password reset: ${error.message}`);
return false;
}
}
@ -958,8 +1064,14 @@ export async function updateUserAttributes(
attributes: Record<string, string[]>,
): Promise<boolean> {
try {
const token = await getToken();
if (!token) {
console.error("[updateUserAttributes] Failed to get Keycloak token");
return false;
}
// Get existing user data to preserve other attributes
const existingUser = await getUser(userId);
const existingUser = await getUser(userId, token);
if (!existingUser) {
console.error(`User ${userId} not found in Keycloak`);
@ -984,7 +1096,7 @@ export async function updateUserAttributes(
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
headers: {
authorization: `Bearer ${await getToken()}`,
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
method: "PUT",

View file

@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddPositionFieldsToProfile1776308026834 implements MigrationInterface {
name = 'AddPositionFieldsToProfile1776308026834'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionField\` varchar(45) NULL COMMENT 'สายงาน'`);
await queryRunner.query(`ALTER TABLE \`profile\` ADD \`posExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร'`);
await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionArea\` varchar(255) NULL COMMENT 'ด้าน/สาขา'`);
await queryRunner.query(`ALTER TABLE \`profile\` ADD \`positionExecutiveField\` varchar(255) NULL COMMENT 'ด้านทางการบริหาร'`);
await queryRunner.query(`ALTER TABLE \`profile\` ADD \`posMasterNo\` varchar(255) NULL COMMENT 'เลขที่ตำแหน่ง'`);
await queryRunner.query(`ALTER TABLE \`profile\` ADD \`org\` text NULL COMMENT 'สังกัด'`);
await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionField\` varchar(45) NULL COMMENT 'สายงาน'`);
await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`posExecutive\` varchar(255) NULL COMMENT 'ตำแหน่งทางการบริหาร'`);
await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionArea\` varchar(255) NULL COMMENT 'ด้าน/สาขา'`);
await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`positionExecutiveField\` varchar(255) NULL COMMENT 'ด้านทางการบริหาร'`);
await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`posMasterNo\` varchar(255) NULL COMMENT 'เลขที่ตำแหน่ง'`);
await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`org\` text NULL COMMENT 'สังกัด'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`org\``);
await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`posMasterNo\``);
await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionExecutiveField\``);
await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionArea\``);
await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`posExecutive\``);
await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`positionField\``);
await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`org\``);
await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`posMasterNo\``);
await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionExecutiveField\``);
await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionArea\``);
await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`posExecutive\``);
await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`positionField\``);
}
}

View file

@ -0,0 +1,186 @@
import { AppDataSource } from "../database/data-source";
import { AuthRoleAttr } from "../entities/AuthRoleAttr";
import { PosMasterAct } from "../entities/PosMasterAct";
export interface ActingPositionData {
isAct: boolean;
posMasterActs: Array<{
privilege: string | null;
posNo: string | null;
rootDnaId: string | null;
child1DnaId: string | null;
child2DnaId: string | null;
child3DnaId: string | null;
child4DnaId: string | null;
}>;
}
export interface ActingPositionWithPrivilegeData extends ActingPositionData {
privilege?: string | null;
}
/**
* Service privilege
*/
export class ActingPositionService {
private posMasterActRepo = AppDataSource.getRepository(PosMasterAct);
private authRoleAttrRepo = AppDataSource.getRepository(AuthRoleAttr);
/**
* privilege
*
* @param profileId - ID profile
* @param orgRevisionId - ID orgRevision
* @param action - Action (CREATE, DELETE, GET, LIST, UPDATE)
* @param system - System ID (authSysId)
* @returns privilege
*/
async getActingPositionsWithPrivilege(
profileId: string,
orgRevisionId: string | undefined,
action?: string,
system?: string
): Promise<ActingPositionWithPrivilegeData> {
// ดึงข้อมูล posMasterAct โดย join กับ posMaster (ตำแหน่งที่ถูกรักษาการ)
const posMasterActs = await this.posMasterActRepo
.createQueryBuilder("posMasterAct")
.leftJoinAndSelect("posMasterAct.posMaster", "posMaster")
.addSelect([
"posMaster.authRoleId", // เพิ่มการดึง authRoleId จากตำแหน่งที่ถูกรักษาการ
"posMaster.posMasterNo", // เพิ่มการดึงเลขที่ตำแหน่ง
"posMaster.posMasterNoPrefix", // เพิ่มการดึง prefix ของเลขที่ตำแหน่ง
"posMaster.posMasterNoSuffix" // เพิ่มการดึง suffix ของเลขที่ตำแหน่ง
])
.leftJoinAndSelect("posMaster.orgRoot", "orgRoot")
.leftJoinAndSelect("posMaster.orgChild1", "orgChild1")
.leftJoinAndSelect("posMaster.orgChild2", "orgChild2")
.leftJoinAndSelect("posMaster.orgChild3", "orgChild3")
.leftJoinAndSelect("posMaster.orgChild4", "orgChild4")
.leftJoinAndSelect("posMaster.orgRevision", "orgRevision")
.leftJoinAndSelect("posMasterAct.posMasterChild", "posMasterChild")
.leftJoinAndSelect("posMasterChild.current_holder", "profileChild")
.where("profileChild.id = :profileId", { profileId })
.andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId })
.andWhere("orgRevision.orgRevisionIsCurrent = true")
.andWhere("orgRevision.orgRevisionIsDraft = false")
.getMany();
if (posMasterActs.length === 0) {
return {
isAct: false,
posMasterActs: [],
};
}
// วนลูปแต่ละ posMasterAct เพื่อดึง privilege ของตำแหน่งที่รักษาการ
const posMasterActsResponse = await Promise.all(
posMasterActs.map(async (act) => {
let privilege: string | null = null;
let privileges: Record<string, string> = {};
if (act.posMaster?.authRoleId) {
// ถ้าระบุ action และ system มา ให้ดึงเฉพาะ privilege ของระบบนั้นๆ
if (action && system) {
const roleAttr = await this.authRoleAttrRepo
.createQueryBuilder("authRoleAttr")
.select(["authRoleAttr.attrPrivilege", "authRoleAttr.attrIsCreate", "authRoleAttr.attrIsDelete", "authRoleAttr.attrIsGet", "authRoleAttr.attrIsList", "authRoleAttr.attrIsUpdate"])
.where("authRoleAttr.authRoleId = :authRoleId", {
authRoleId: act.posMaster.authRoleId,
})
.andWhere("authRoleAttr.authSysId = :system", { system })
.getOne();
if (roleAttr) {
// ตรวจสอบสิทธิ์ตาม action
let hasPermission = false;
const actionUpper = action.trim().toUpperCase();
switch (actionUpper) {
case "CREATE":
hasPermission = roleAttr.attrIsCreate;
break;
case "DELETE":
hasPermission = roleAttr.attrIsDelete;
break;
case "GET":
hasPermission = roleAttr.attrIsGet;
break;
case "LIST":
hasPermission = roleAttr.attrIsList;
break;
case "UPDATE":
hasPermission = roleAttr.attrIsUpdate;
break;
}
if (hasPermission) {
privilege = roleAttr.attrPrivilege;
}
}
} else {
// ดึงข้อมูล AuthRoleAttr สำหรับทุกระบบ
const roleAttrs = await this.authRoleAttrRepo
.createQueryBuilder("authRoleAttr")
.select(["authRoleAttr.authSysId", "authRoleAttr.attrPrivilege"])
.where("authRoleAttr.authRoleId = :authRoleId", {
authRoleId: act.posMaster.authRoleId,
})
.getMany();
privileges = roleAttrs.reduce((acc, attr) => {
acc[attr.authSysId] = attr.attrPrivilege;
return acc;
}, {} as Record<string, string>);
}
}
// จัดรูปแบบเลขที่ตำแหน่งตามรูปแบบ shortName ที่ใช้ในระบบ
const holder = act.posMaster;
const posNo = !holder
? null
: holder.orgChild4 != null
? `${holder.orgChild4.orgChild4ShortName} ${holder.posMasterNo}`
: holder.orgChild3 != null
? `${holder.orgChild3.orgChild3ShortName} ${holder.posMasterNo}`
: holder.orgChild2 != null
? `${holder.orgChild2.orgChild2ShortName} ${holder.posMasterNo}`
: holder.orgChild1 != null
? `${holder.orgChild1.orgChild1ShortName} ${holder.posMasterNo}`
: holder.orgRoot != null
? `${holder.orgRoot.orgRootShortName} ${holder.posMasterNo}`
: null;
return {
posNo: posNo,
privilege: action && system ? privilege : JSON.stringify(privileges),
rootDnaId: act.posMaster?.orgRoot?.ancestorDNA ?? null,
child1DnaId: act.posMaster?.orgChild1?.ancestorDNA ?? null,
child2DnaId: act.posMaster?.orgChild2?.ancestorDNA ?? null,
child3DnaId: act.posMaster?.orgChild3?.ancestorDNA ?? null,
child4DnaId: act.posMaster?.orgChild4?.ancestorDNA ?? null,
};
})
);
// ถ้าระบุ action และ system มา ให้ดึง privilege ของตำแหน่งแรก
let specificPrivilege: string | null = null;
if (action && system && posMasterActsResponse.length > 0) {
specificPrivilege = posMasterActsResponse[0].privilege;
}
const response: ActingPositionWithPrivilegeData = {
isAct: true,
posMasterActs: posMasterActsResponse,
};
// ถ้าระบุ action และ system มา ให้เพิ่ม privilege เข้าไปใน response ด้วย
if (action && system) {
response.privilege = specificPrivilege ?? null;
}
return response;
}
}
// Export singleton instance
export const actingPositionService = new ActingPositionService();

View file

@ -442,6 +442,223 @@ export class KeycloakAttributeService {
}
}
/**
* Check if Keycloak user has empty/null empType attribute
* @param keycloakUserId - Keycloak user ID
* @returns Object with isEmpty flag and currentEmpType value
*/
async checkEmpTypeEmpty(keycloakUserId: string): Promise<{
isEmpty: boolean;
currentEmpType?: string;
}> {
try {
const user = await getUser(keycloakUserId);
if (!user || !user.attributes) {
return { isEmpty: true };
}
const empType = user.attributes.empType?.[0];
return {
isEmpty: !empType || empType.trim() === "",
currentEmpType: empType || "",
};
} catch (error) {
console.error(`[checkEmpTypeEmpty] Error for user ${keycloakUserId}:`, error);
return { isEmpty: true }; // Assume empty on error
}
}
/**
* Sync profiles with missing empType for a specific month
* @param options - Sync configuration
* @returns Sync results summary
*/
async syncMissingEmpTypeByMonth(options: {
month: string; // "YYYY-MM" format
profileType?: "PROFILE" | "PROFILE_EMPLOYEE";
dryRun?: boolean;
concurrency?: number;
rateLimit?: number;
}): Promise<{
month: string;
profileType: string;
totalProfiles: number;
profilesChecked: number;
missingEmpType: number;
syncSuccess: number;
syncFailed: number;
skipped: number;
executionTime: string;
dryRun: boolean;
}> {
const startTime = Date.now();
const {
month,
profileType = "PROFILE",
dryRun = false,
concurrency = 5,
rateLimit = 10,
} = options;
const result = {
month,
profileType,
totalProfiles: 0,
profilesChecked: 0,
missingEmpType: 0,
syncSuccess: 0,
syncFailed: 0,
skipped: 0,
executionTime: "",
dryRun,
};
let rateLimiter: RateLimiter | null = null;
try {
// Parse month (YYYY-MM) to date range
const [year, monthNum] = month.split("-").map(Number);
const startDate = new Date(Date.UTC(year, monthNum - 1, 1, 0, 0, 0));
const endDate = new Date(Date.UTC(year, monthNum, 0, 23, 59, 59, 999));
console.log(
`[syncMissingEmpTypeByMonth] Processing ${profileType} for ${month} (${startDate.toISOString()} to ${endDate.toISOString()})`,
);
// Initialize rate limiter if rate limiting is enabled
if (rateLimit && rateLimit > 0) {
rateLimiter = new RateLimiter(rateLimit);
console.log(`[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`);
}
// Select repository based on profile type
const repo =
profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo;
// Query profiles updated within the month
const profiles = await repo
.createQueryBuilder("p")
.where("p.keycloak IS NOT NULL")
.andWhere("p.keycloak != :empty", { empty: "" })
.andWhere("p.lastUpdatedAt BETWEEN :start AND :end", {
start: startDate,
end: endDate,
})
.orderBy("p.lastUpdatedAt", "ASC")
.getMany();
result.totalProfiles = profiles.length;
console.log(`[syncMissingEmpTypeByMonth] Found ${profiles.length} profiles to check`);
if (profiles.length === 0) {
result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`;
return result;
}
// Process profiles in parallel with concurrency limit
for (let i = 0; i < profiles.length; i += concurrency) {
const batch = profiles.slice(i, i + concurrency);
await Promise.all(
batch.map(async (profile) => {
// Apply rate limiting if enabled
if (rateLimiter) {
await rateLimiter.throttle();
}
const keycloakUserId = profile.keycloak;
if (!keycloakUserId) {
return {
profileId: profile.id,
status: "skipped" as const,
reason: "No keycloak ID",
};
}
try {
// Check if empType is empty in Keycloak
const { isEmpty, currentEmpType } =
await this.checkEmpTypeEmpty(keycloakUserId);
result.profilesChecked++;
if (!isEmpty) {
result.skipped++;
return {
profileId: profile.id,
status: "skipped" as const,
reason: "empType already exists",
empType: currentEmpType,
};
}
result.missingEmpType++;
if (dryRun) {
return {
profileId: profile.id,
status: "skipped" as const,
reason: "dry run",
wouldSync: true,
};
}
// Sync the profile
const success = await withRetry(
async () =>
this.syncOnOrganizationChange(profile.id, profileType),
3, // maxRetries
1000, // baseDelay
);
if (success) {
result.syncSuccess++;
return {
profileId: profile.id,
status: "synced" as const,
};
} else {
result.syncFailed++;
return {
profileId: profile.id,
status: "failed" as const,
reason: "Sync returned false",
};
}
} catch (error: any) {
result.syncFailed++;
return {
profileId: profile.id,
status: "failed" as const,
reason: error.message || "Unknown error",
};
}
}),
);
// Log progress every 50 profiles
const completed = Math.min(i + concurrency, profiles.length);
if (completed % 50 === 0 || completed === profiles.length) {
console.log(
`[syncMissingEmpTypeByMonth] Progress: ${completed}/${profiles.length} profiles processed`,
);
}
}
result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`;
console.log(
`[syncMissingEmpTypeByMonth] Completed: total=${result.totalProfiles}, checked=${result.profilesChecked}, missing=${result.missingEmpType}, synced=${result.syncSuccess}, failed=${result.syncFailed}, skipped=${result.skipped}, elapsed=${result.executionTime}`,
);
} catch (error) {
console.error("[syncMissingEmpTypeByMonth] Error:", error);
throw error;
}
return result;
}
/**
* Clear org DNA attributes in Keycloak for given profiles
* Sets all org DNA fields to empty strings

View file

@ -3,6 +3,7 @@ import { AppDataSource } from "../database/data-source";
import { Command } from "../entities/Command";
import { chunkArray, commandTypePath } from "../interfaces/utils";
import CallAPI from "../interfaces/call-api";
import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { PosMaster } from "../entities/PosMaster";
@ -651,23 +652,33 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
await posMasterAssignRepository.save(newAssigns);
}
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (item.next_holderId != null && !item.isSit) {
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
if (item.next_holderId != null) {
const profile = await repoProfile.findOne({
where: { id: item.next_holderId == null ? "" : item.next_holderId },
});
if (profile != null && item.positions.length > 0) {
let position = await item.positions.find((x) => x.positionIsSelected == true);
if (position == null) {
position = await item.positions.find((x) => x.posLevelId == profile?.posLevelId);
if (position == null) {
position = await item.positions.sort((a, b) => a.orderNo - b.orderNo)[0];
}
}
if (profile != null) {
profile.posMasterNo = getPosMasterNo(item) ?? _null;
profile.org = getOrgFullName(item) ?? _null;
profile.posLevelId = position?.posLevelId ?? _null;
profile.posTypeId = position?.posTypeId ?? _null;
profile.position = position?.positionName ?? _null;
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (!item.isSit && item.positions.length > 0) {
let position = await item.positions.find((x) => x.positionIsSelected == true);
if (position == null) {
position = await item.positions.find((x) => x.posLevelId == profile?.posLevelId);
if (position == null) {
position = await item.positions.sort((a, b) => a.orderNo - b.orderNo)[0];
}
}
profile.posLevelId = position?.posLevelId ?? _null;
profile.posTypeId = position?.posTypeId ?? _null;
profile.position = position?.positionName ?? _null;
profile.positionField = position?.positionField ?? _null;
profile.posExecutive = position?.posExecutive?.posExecutiveName ?? _null;
profile.positionArea = position?.positionArea ?? _null;
profile.positionExecutiveField = position?.positionExecutiveField ?? _null;
}
await repoProfile.save(profile);
}
}

View file

@ -68,3 +68,47 @@ export function filterPosMasters(
): PosMaster[] {
return posMasters.filter((x) => x[childLevelIdKey] == null && x.isDirector === true);
}
/**
* orgShortName posMaster ( load org relations )
*/
export function getOrgShortName(posMaster: PosMaster): string {
if (posMaster.orgChild1Id === null) {
return posMaster.orgRoot?.orgRootShortName ?? "";
} else if (posMaster.orgChild2Id === null) {
return posMaster.orgChild1?.orgChild1ShortName ?? "";
} else if (posMaster.orgChild3Id === null) {
return posMaster.orgChild2?.orgChild2ShortName ?? "";
} else if (posMaster.orgChild4Id === null) {
return posMaster.orgChild3?.orgChild3ShortName ?? "";
} else {
return posMaster.orgChild4?.orgChild4ShortName ?? "";
}
}
/**
* posMaster (join \n)
*/
export function getOrgFullName(posMaster: PosMaster): string {
const parts = [
posMaster.orgChild4?.orgChild4Name,
posMaster.orgChild3?.orgChild3Name,
posMaster.orgChild2?.orgChild2Name,
posMaster.orgChild1?.orgChild1Name,
posMaster.orgRoot?.orgRootName,
];
return parts.filter((part) => part !== undefined && part !== null).join("\n");
}
/**
* "กทม. กบ.1234ช"
*/
export function getPosMasterNo(posMaster: PosMaster): string {
const orgShortName = getOrgShortName(posMaster);
const parts = [
posMaster.posMasterNoPrefix,
posMaster.posMasterNo,
posMaster.posMasterNoSuffix,
].filter((part) => part !== null && part !== undefined);
return `${orgShortName} ${parts.join('')}`;
}

23
src/utils/tenure.ts Normal file
View file

@ -0,0 +1,23 @@
/**
*
* @param totalDays
* @returns { year, month, day }
*/
export function calculateTenure(totalDays: number) {
// 1. แปลงเป็น year เต็ม
const year = Math.floor(totalDays / 365.2524);
// 2. วันที่เหลือหลังหัก year ออก
const remainAfterYear = totalDays - year * 365.2524;
// 3. แปลงเป็น month เต็ม
const month = Math.floor(remainAfterYear / 30.4375);
// 4. วันที่เหลือหลังหัก month ออก
const remainAfterMonth = remainAfterYear - month * 30.4375;
// 5. ปัดลง เฉพาะวัน
const day = Math.floor(remainAfterMonth);
return { year, month, day };
}