Compare commits

...

100 commits
v1.1.90 ... dev

Author SHA1 Message Date
harid
825263c11c API ตรวจสอบสถานะผู้สมัครสอบ #2518
All checks were successful
Build & Deploy on Dev / build (push) Successful in 58s
2026-06-04 17:40:36 +07:00
664f5153da Merge branch 'develop' into adiDev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m3s
2026-06-04 09:25:29 +07:00
219a2908a3 แก้ format ฟิว posMasterNo (5) สำหรับบันทึกลงระบบที่พัฒนาบน .net
1.ปรับระดับชั้นงาน-ย้ายลูกจ้าง (เลข Prefix / Suffix ไม่แสดง)
2.รายการอื่นๆ (เลข Prefix / Suffix ไม่แสดง)
3.รายการลาออก (เลข Prefix / Suffix ไม่แสดง)
4.รายการลาออกลูกจ้างฯ (เลข Prefix / Suffix ไม่แสดง)
5.เรื่องร้องเรียน (เลข Prefix / Suffix ไม่แสดง)
6.สืบสวนฯ (เลข Prefix / Suffix ไม่แสดง)
7.สอบสวนฯ (เลข Prefix / Suffix ไม่แสดง)
8.สรุปผลการพิจารณาฯ (เลข Prefix / Suffix ไม่แสดง)
2026-06-04 09:25:01 +07:00
harid
b0cfbc7036 fix #2510
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-05-29 17:37:19 +07:00
185aedc53f remove log success 2026-05-29 17:23:45 +07:00
20c6c412b8 remove log success 2026-05-29 14:24:50 +07:00
harid
ad9a7dcbb6 fix ระบบไม่ปั๊มประวัติข้อมูลเดิมให้ row แรก #2535
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
2026-05-29 14:08:25 +07:00
harid
774a58bc22 Merge branch 'develop' into develop-Bright 2026-05-29 10:36:56 +07:00
755ae992dd #246 เสริมคำสั่ง C-PM-25,C-PM-26
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
2026-05-29 10:12:46 +07:00
harid
d495137aaf devTest Controller สำหรับทดสอบ 2026-05-28 17:11:06 +07:00
harid
521a748de1 fix แก้ไขประวัติส่วนตัว ไม่ปั๊มประวัติรายการคนครอง #244
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m9s
2026-05-28 13:02:58 +07:00
ccfb2754fd API apiKey list return accessType and orgName
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
2026-05-28 09:49:21 +07:00
95aad0b9fb Merge branch 'develop' into adiDev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m9s
2026-05-28 09:09:31 +07:00
399bf87ba6 #246 เคลีนร์ isSit 2026-05-28 09:08:58 +07:00
harid
9782871c9c ส่งรายชื่ออกคำสั่งวินัย เพิ่ม CommandCode, CommandId #2377
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
2026-05-27 17:46:44 +07:00
harid
a36ec74e84 รายงานแนบท้าย ข้อมูล "ลงวันที่" หัวกระดาษ ให้ใช้ข้อมูล "วันที่ลงนาม" #2514 2026-05-27 16:51:03 +07:00
harid
7d463806a9 ปั๊มประวัติคนครองกรณีมีแก้ไขชื่อและนามสกุล #244
All checks were successful
Build & Deploy on Dev / build (push) Successful in 58s
2026-05-27 16:41:56 +07:00
a678f95075 revert #2533
All checks were successful
Build & Deploy on Dev / build (push) Successful in 57s
2026-05-27 14:58:00 +07:00
c6efd34e95 Merge branch 'adiDev' into develop
All checks were successful
Build & Deploy on Dev / build (push) Successful in 56s
2026-05-27 14:25:09 +07:00
fa2d922fc3 #2529 , #2533 2026-05-27 14:24:32 +07:00
harid
1e68045793 Merge branch 'develop' into develop-Bright
All checks were successful
Build & Deploy on Dev / build (push) Successful in 56s
2026-05-27 12:05:39 +07:00
harid
7ebd01ef19 กรอง "." ออกจาก firstName ก่อนส่งไป keycloak #2517 2026-05-27 12:05:12 +07:00
59c5cfb9bf #2527
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m1s
2026-05-27 11:13:40 +07:00
238c4c092f fix search concat_ws skip ' '
All checks were successful
Build & Deploy on Dev / build (push) Successful in 59s
2026-05-27 09:51:10 +07:00
8b08f8b5c8 fix search
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m3s
2026-05-26 18:34:47 +07:00
9288c9e833 Merge branch 'develop' into adiDev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m14s
2026-05-26 18:11:00 +07:00
cc6696cec8 fix: searh org 2026-05-26 18:10:34 +07:00
c87b604685 fixed bug api service leave type name
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
2026-05-26 17:37:08 +07:00
harid
0aa788fe0b fix search commandNo #242
All checks were successful
Build & Deploy on Dev / build (push) Successful in 59s
2026-05-26 16:42:17 +07:00
harid
bc418666ac fix search commandNo #242
All checks were successful
Build & Deploy on Dev / build (push) Successful in 59s
2026-05-26 16:13:53 +07:00
harid
2e217a9548 no message
All checks were successful
Build & Deploy on Dev / build (push) Successful in 56s
2026-05-26 15:50:22 +07:00
f06be7ce77 fixes for ProfileLeave queries. Changed leaveTypeId to return leaveTypeName from LeaveType master data and added isDeleted filtering for ProfileLeave and 37 other Profile entities.
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m3s
2026-05-26 15:35:35 +07:00
harid
81e8dadd9b Migrate update_command_add_shortName #242
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
2026-05-26 15:30:51 +07:00
harid
136a4c562e Merge branch 'develop' into develop-Bright 2026-05-26 13:20:22 +07:00
harid
0cad83af1f #2523 STAFF + isDirector ให้ล้อสิทธิ์เหมือน CHILD 2026-05-26 13:20:02 +07:00
97df4d6cf5 fixed api web service explode field 2026-05-26 12:43:11 +07:00
82d81334f5 #2505
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m6s
2026-05-25 18:19:34 +07:00
harid
7b22fb2a2d fix api key
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
2026-05-25 17:43:50 +07:00
harid
2868c329e1 Merge branch 'develop' into develop-Bright
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m6s
2026-05-25 17:35:00 +07:00
harid
eede5f51c4 @Route("api/v1/org/dotnet") ปรับใช้ API Key 2026-05-25 17:34:24 +07:00
1555e59b88 fixed BATCH_SIZE 100 to 50 2026-05-25 17:31:58 +07:00
32282b016b remove log updateUserAttributes success
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m8s
2026-05-23 09:17:49 +07:00
61a09acbad fixed bug
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
2026-05-23 00:30:57 +07:00
e0f2513ba4 add log check isSelected in position
Some checks failed
Build & Deploy on Dev / build (push) Failing after 39s
2026-05-23 00:23:55 +07:00
0c7c8e9fd3 fixed api web service case select ROOT, NORMAL, CHILD
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m0s
2026-05-22 21:57:25 +07:00
33bd92af11 complete api web service registry add org id
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
2026-05-22 21:36:46 +07:00
605c48be57 Merge branch 'develop' of github.com:Frappet/bma-ehr-organization into develop
* 'develop' of github.com:Frappet/bma-ehr-organization:
  เพิ่ม log api excexute/create-officer-profile
2026-05-22 21:30:51 +07:00
2abbc6225e fixed api web service 2026-05-22 21:30:38 +07:00
harid
4cd39bb0e9 เพิ่ม log api excexute/create-officer-profile
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-05-22 18:22:00 +07:00
harid
2ce104b852 condition ที่ชื่อตำแหน่ง, ประเภท และ ระดับซ้ำกัน ให้ใช้แถวลำดับแรก
All checks were successful
Build & Deploy on Dev / build (push) Successful in 55s
2026-05-22 13:55:55 +07:00
harid
ce114cf769 API เรียงลำดับทะเบียนประวัติและเงินเดือนที่กำลังแก้ไขตามวันที่คำสั่งมีผล #2509 2026-05-22 13:38:21 +07:00
4c9ed3d317 Merge branch 'adiDev' into develop
All checks were successful
Build & Deploy on Dev / build (push) Successful in 55s
2026-05-22 13:33:39 +07:00
442ce20d80 #1588 and performance 2026-05-22 13:33:22 +07:00
6719585d45 fixed api service 2026-05-22 12:35:21 +07:00
bcc27002db Merge branch 'feat/web-service-external' into develop
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
* feat/web-service-external:
  fix api web service
2026-05-21 18:08:41 +07:00
d3f01165ae fix api web service 2026-05-21 18:08:20 +07:00
harid
3d01a166a8 fix เลือกตำแหน่งในสายงานหาย เมื่อมีการเปลี่ยนแปลงข้อมูลตำแหน่งในโครงสร้าง #2505
All checks were successful
Build & Deploy on Dev / build (push) Successful in 57s
2026-05-21 16:51:55 +07:00
ba185f8de8 แก้ format ฟิว posMasterNo (4) สำหรับบันทึกลงระบบที่พัฒนาบน .net
All checks were successful
Build & Deploy on Dev / build (push) Successful in 58s
2026-05-21 16:30:24 +07:00
fa63953d75 แก้ format ฟิว posMasterNo (3) สำหรับบันทึกลงระบบที่พัฒนาบน .net
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m1s
2026-05-21 15:47:07 +07:00
8c9a62a378 Merge branch 'develop' into adiDev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m3s
2026-05-21 14:24:47 +07:00
69a7ddaaa3 แก้ format ฟิว posMasterNo (2) 2026-05-21 14:23:59 +07:00
00d043b1fa Merge branch 'develop' into feat/web-service-external
All checks were successful
Build & Deploy on Dev / build (push) Successful in 58s
* develop:
  แก้ format ฟิว posMasterNo (1)
2026-05-21 14:03:56 +07:00
b7c80ea6d4 fixed error 2026-05-21 14:03:30 +07:00
44793fbfbb api web service add join for show name 2026-05-21 13:44:03 +07:00
b071bc2d92 api service add filter by dnaId of Profile 2026-05-21 11:44:28 +07:00
6afac07f74 Merge branch 'develop' into adiDev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m3s
2026-05-21 11:26:58 +07:00
168abb1255 แก้ format ฟิว posMasterNo (1) 2026-05-21 11:26:29 +07:00
b2d59ef698 แก้ไขประเภท ระดับตำแหน่ง และจังหวัด อำเภอ ตำบลให้แสดงฟิลด์ที่เป็นชื่อให้เลือก 2026-05-21 11:07:15 +07:00
9fe91ce49c ตัดฟิลด์ซ้ำและฟิลด์ id ออก 2026-05-21 10:53:45 +07:00
12e8cdb080 fixed bug sync org to leave service 2026-05-21 10:30:09 +07:00
e374f2e339 fixed service crash
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
2026-05-20 20:40:09 +07:00
bca04f2881 fixed Promise.all Without Error Handling 2026-05-20 20:23:41 +07:00
9a5184bb55 add script delete org old 2026-05-20 17:51:15 +07:00
a28c099f86 Merge branch 'develop' of github.com:Frappet/bma-ehr-organization into develop
* 'develop' of github.com:Frappet/bma-ehr-organization:
  fix api sort 504 time out
2026-05-20 17:40:14 +07:00
36b1469016 fix api sort 504 time out
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m7s
2026-05-20 17:33:02 +07:00
300f073638 fixed bug Redis Client Connection Leak 2026-05-20 17:12:16 +07:00
d0c5d90033 #2508
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
2026-05-20 16:27:39 +07:00
harid
ddf0309b0b fix ส่งรายชื่อบรรจุ คำนำหน้าไม่แสดง #2506
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
2026-05-20 15:53:10 +07:00
harid
5dafd13124 API ดึงรายชื่อลูกจ้างประจำตามประวัติตำแหน่ง #2385
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m7s
2026-05-20 14:35:37 +07:00
0e80acfb1b ลบ log ออกเพื่อให้สามารถดู log อื่นๆ ได้ง่ายขึ้น 2026-05-20 11:26:27 +07:00
harid
e04d1ad7d3 Migrate + API ดึงรายชื่อลูกจ้างประจำตามประวัติตำแหน่ง
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
2026-05-20 10:57:18 +07:00
harid
458c9b1042 fix เมนูจัดการผู้ใช้งาน #2471
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m1s
2026-05-19 16:23:29 +07:00
harid
a8f7554302 test 2026-05-19 10:33:50 +07:00
b7f7b907bf fix executive store procedure 2026-05-19 06:58:21 +07:00
6c5356ca46 fixed tenure
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m20s
2026-05-18 23:25:09 +07:00
5ea111a3c5 fixed cron job 2026-05-18 21:51:23 +07:00
d093953fbe แก้ไขการคำนวนระยะเวลาครองตำแหน่ง 2026-05-18 20:56:20 +07:00
f1c546ba8f fix store procedure 2026-05-18 17:37:43 +07:00
harid
15830ef2ba fix ยศไม่แสดงในระบบทะเบียน #2469 + Error Log api check-keycloak
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m8s
2026-05-18 15:02:38 +07:00
harid
173378d87c fix ยศไม่แสดงในระบบทะเบียนประวัติหลังออกคำสั่งรับโอนเสร็จสิ้น #2469
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m9s
2026-05-18 09:18:07 +07:00
harid
7985125882 api check keycloak for process check in
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m7s
2026-05-15 14:58:13 +07:00
b103e15788 fixed web socket noti by token
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m1s
2026-05-15 14:33:00 +07:00
harid
74d03176cd fix ระบบแจ้ง Noti ไม่ตรงตามสิทธิ์ที่ได้รับ #2488
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s
2026-05-15 11:53:14 +07:00
harid
9f2fec3ee3 fix ระบบแจ้ง Noti สิทธิ์ BROTHER
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m7s
2026-05-15 11:12:17 +07:00
harid
3c8b377764 fix ระบบแจ้ง Noti ไม่ตรงตามสิทธิ์ที่ได้รับ #2488
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m6s
2026-05-14 17:15:39 +07:00
cab2f76bd6 add api notify-from-token
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-05-14 12:33:21 +07:00
af2bd5054f feat: clear menu and role cache when organization structure is published
Add Redis cache clearing to handler_org function to clear all menu_* and role_* keys
after successfully publishing organization structure changes. This ensures users
see updated permissions and menus immediately after publish.

- Add promisify import and Redis client setup
- Add clearMenuAndRoleCache helper function
- Call cache clearing before successful return

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:37:57 +07:00
94edcf5320 fix act position condition
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m6s
2026-05-12 23:12:01 +07:00
334ce4f5fc fixed #2413 จำนวนวันอายุราชการแสดงไม่ตรงกัน
All checks were successful
Build & Deploy on Dev / build (push) Successful in 58s
2026-05-12 17:24:15 +07:00
DESKTOP-1R2VSQH\Lenovo ThinkPad E490
e64cd3f384 fix: permission
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m11s
2026-05-12 15:14:21 +07:00
55 changed files with 7483 additions and 2419 deletions

View file

@ -0,0 +1,140 @@
-- ====================================================================
-- Fix GetProfileEmployeeSalaryLevel to use calendar arithmetic
-- This changes from fixed formulas to actual calendar arithmetic,
-- matching calculateGovAge and GetProfileSalaryLevel behavior
-- ====================================================================
DELIMITER $$
DROP PROCEDURE IF EXISTS `GetProfileEmployeeSalaryLevel`$$
CREATE DEFINER=`root`@`%` PROCEDURE `GetProfileEmployeeSalaryLevel`(
IN personId VARCHAR(36),
IN _date DATE
)
BEGIN
WITH ordered AS (
SELECT *
FROM profileSalary
WHERE profileEmployeeId = personId
AND commandCode IN ('0','1','2','3','4','8','9','10','11','12','13','14','15','16','20')
),
work_session AS (
SELECT *,
COALESCE(
SUM(CASE WHEN commandCode IN (12,15,16) THEN 1 ELSE 0 END)
OVER (ORDER BY commandDateAffect, commandDateSign
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),
0) AS sessionId
FROM ordered
),
session_end AS (
SELECT sessionId, MAX(commandDateAffect) AS sessionEndDate
FROM work_session
GROUP BY sessionId
),
level_change AS (
SELECT *,
CASE
WHEN LAG(positionCee) OVER (ORDER BY commandDateAffect, commandDateSign) <=> positionCee
AND LAG(positionType) OVER (ORDER BY commandDateAffect, commandDateSign) <=> positionType
AND LAG(positionLevel) OVER (ORDER BY commandDateAffect, commandDateSign) <=> positionLevel
AND LAG(sessionId) OVER (ORDER BY commandDateAffect, commandDateSign) = sessionId
THEN 0
ELSE 1
END AS isNewLevel
FROM work_session
),
level_group AS (
SELECT *,
SUM(isNewLevel) OVER (ORDER BY commandDateAffect, commandDateSign) AS levelGroup
FROM level_change
),
first_rows AS (
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY levelGroup ORDER BY commandDateAffect, commandDateSign) AS rnLevel
FROM level_group
) t WHERE rnLevel = 1
),
rows_with_duration AS (
SELECT
fr.*,
CASE
WHEN LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) IS NULL
THEN NULL
WHEN LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) <> fr.sessionId
THEN TIMESTAMPDIFF(DAY, fr.commandDateAffect, se.sessionEndDate) + 1
ELSE
TIMESTAMPDIFF(DAY, fr.commandDateAffect,
LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign))
END AS duration_days
FROM first_rows fr
LEFT JOIN session_end se ON se.sessionId = fr.sessionId
),
resultWithDiff AS (
SELECT
*,
LAG(duration_days) OVER (ORDER BY commandDateAffect, commandDateSign) AS days_diff
FROM rows_with_duration
)
SELECT
r.commandDateAffect,
r.positionType,
r.positionLevel,
r.positionCee,
r.days_diff,
CASE
WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect)
ELSE 0
END AS Years,
CASE
WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12
ELSE 0
END AS Months,
CASE
WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN
DATEDIFF(r.commandDateAffect,
DATE_ADD(
DATE_ADD(LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign),
INTERVAL TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12 MONTH)
)
ELSE 0
END AS Days,
r.posNo,
r.positionExecutive,
r.orgRoot,
r.orgChild1,
r.orgChild2,
r.orgChild3,
r.orgChild4,
r.commandCode,
r.commandName,
r.commandNo,
r.commandYear,
r.remark
FROM resultWithDiff r
UNION ALL
SELECT
_date, NULL, NULL, NULL,
TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1,
TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date),
TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12,
DATEDIFF(_date,
DATE_ADD(
DATE_ADD(MAX(commandDateAffect),
INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH)
),
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
NULL,NULL,NULL,NULL
FROM resultWithDiff;
END$$
DELIMITER ;

View file

@ -0,0 +1,137 @@
-- ====================================================================
-- Fix GetProfileEmployeeSalaryPosition to use calendar arithmetic
-- This changes from fixed formulas to actual calendar arithmetic,
-- matching calculateGovAge and GetProfileSalaryPosition behavior
-- ====================================================================
DELIMITER $$
DROP PROCEDURE IF EXISTS `GetProfileEmployeeSalaryPosition`$$
CREATE DEFINER=`root`@`%` PROCEDURE `GetProfileEmployeeSalaryPosition`(
IN personId VARCHAR(36),
IN _date DATE
)
BEGIN
WITH ordered AS (
SELECT * FROM profileSalary WHERE profileEmployeeId = personId AND commandCode IN ('0','1','2','3','4','8','9','10','11','12','13','14','15','16','20')
),
work_session AS (
SELECT *,
COALESCE(
SUM(CASE WHEN commandCode IN (12,15,16) THEN 1 ELSE 0 END)
OVER (ORDER BY commandDateAffect, commandDateSign
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),
0) AS sessionId
FROM ordered
),
session_end AS (
SELECT sessionId, MAX(commandDateAffect) AS sessionEndDate
FROM work_session
GROUP BY sessionId
),
position_change AS (
SELECT *,
CASE
WHEN LAG(positionName) OVER (ORDER BY commandDateAffect, commandDateSign) = positionName
AND LAG(sessionId) OVER (ORDER BY commandDateAffect, commandDateSign) = sessionId
THEN 0
ELSE 1
END AS isNewPosition
FROM work_session
),
position_group AS (
SELECT *,
SUM(isNewPosition) OVER (ORDER BY commandDateAffect, commandDateSign) AS posGroup
FROM position_change
),
first_rows AS (
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY posGroup ORDER BY commandDateAffect, commandDateSign) AS rnPos
FROM position_group
) t WHERE rnPos = 1
),
rows_with_duration AS (
SELECT
fr.*,
LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) AS nextSessionId,
CASE
WHEN LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) IS NULL
THEN NULL
WHEN LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) <> fr.sessionId
THEN TIMESTAMPDIFF(DAY, fr.commandDateAffect, se.sessionEndDate) + 1
ELSE
TIMESTAMPDIFF(DAY, fr.commandDateAffect,
LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign))
END AS duration_days
FROM first_rows fr
LEFT JOIN session_end se ON se.sessionId = fr.sessionId
),
resultWithDiff AS (
SELECT
*,
LAG(duration_days) OVER (ORDER BY commandDateAffect, commandDateSign) AS days_diff
FROM rows_with_duration
)
SELECT
r.commandDateAffect,
r.positionName,
r.positionCee,
r.days_diff,
CASE
WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect)
ELSE 0
END AS Years,
CASE
WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12
ELSE 0
END AS Months,
CASE
WHEN LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(DAY,
DATE_ADD(
DATE_ADD(LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign),
INTERVAL TIMESTAMPDIFF(YEAR, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, LAG(commandDateAffect) OVER (ORDER BY commandDateAffect, commandDateSign), r.commandDateAffect) % 12 MONTH),
r.commandDateAffect)
ELSE 0
END AS Days,
r.posNo,
r.positionExecutive,
r.positionType,
r.positionLevel,
r.orgRoot,
r.orgChild1,
r.orgChild2,
r.orgChild3,
r.orgChild4,
r.commandCode,
r.commandName,
r.commandNo,
r.commandYear,
r.remark
FROM resultWithDiff r
UNION ALL
SELECT
_date, NULL, NULL,
TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1,
TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date),
TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12,
DATEDIFF(_date,
DATE_ADD(
DATE_ADD(MAX(commandDateAffect),
INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH)
),
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
NULL,NULL,NULL,NULL,NULL
FROM resultWithDiff;
END$$
DELIMITER ;

View file

@ -0,0 +1,136 @@
-- ====================================================================
-- Fix GetProfileSalaryExecutive to use calendar arithmetic
-- This changes the years/months/days calculation from fixed formulas
-- to actual calendar arithmetic, matching calculateGovAge behavior
-- ====================================================================
DELIMITER $$
DROP PROCEDURE IF EXISTS `GetProfileSalaryExecutive`$$
CREATE DEFINER=`root`@`%` PROCEDURE `GetProfileSalaryExecutive`(
IN personId VARCHAR(36),
IN _date DATE
)
BEGIN
WITH ordered AS (
SELECT * FROM profileSalary WHERE profileId = personId AND commandCode IN ('0','1','2','3','4','8','9','10','11','12','13','14','15','16','20') AND positionExecutive <> ''
),
work_session AS (
SELECT *,
COALESCE(
SUM(CASE WHEN commandCode IN (12,15,16) THEN 1 ELSE 0 END)
OVER (ORDER BY commandDateAffect, commandDateSign
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),
0) AS sessionId
FROM ordered
),
session_end AS (
SELECT sessionId, MAX(commandDateAffect) AS sessionEndDate
FROM work_session
GROUP BY sessionId
),
executive_change AS (
SELECT *,
CASE
WHEN LAG(positionExecutive) OVER (ORDER BY commandDateAffect, commandDateSign) = positionExecutive
AND LAG(sessionId) OVER (ORDER BY commandDateAffect, commandDateSign) = sessionId
THEN 0
ELSE 1
END AS isNewExecutive
FROM work_session
),
executive_group AS (
SELECT *,
SUM(isNewExecutive) OVER (ORDER BY commandDateAffect, commandDateSign) AS execGroup
FROM executive_change
),
first_rows AS (
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY execGroup ORDER BY commandDateAffect, commandDateSign) AS rnExec
FROM executive_group
) t WHERE rnExec = 1
),
rows_with_duration AS (
SELECT
fr.*,
LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) AS nextSessionId,
CASE
WHEN LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) IS NULL
THEN NULL
WHEN LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) <> fr.sessionId
THEN TIMESTAMPDIFF(DAY, fr.commandDateAffect, se.sessionEndDate) + 1
ELSE
TIMESTAMPDIFF(DAY, fr.commandDateAffect,
LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign))
END AS duration_days
FROM first_rows fr
LEFT JOIN session_end se ON se.sessionId = fr.sessionId
),
resultWithDiff AS (
SELECT
*,
LAG(duration_days) OVER (ORDER BY commandDateAffect, commandDateSign) AS days_diff
FROM rows_with_duration
)
SELECT
r.commandDateAffect,
r.positionExecutive,
r.days_diff,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect)
ELSE 0
END AS Years,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12
ELSE 0
END AS Months,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
DATEDIFF(r.commandDateAffect,
DATE_ADD(
DATE_ADD(LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign),
INTERVAL TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12 MONTH)
)
ELSE 0
END AS Days,
r.posNo,
r.positionType,
r.positionLevel,
r.positionCee,
r.orgRoot,
r.orgChild1,
r.orgChild2,
r.orgChild3,
r.orgChild4,
r.commandCode,
r.commandName,
r.commandNo,
r.commandYear,
r.remark
FROM resultWithDiff r
UNION ALL
SELECT
_date, NULL,
TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1,
TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date),
TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12,
DATEDIFF(_date,
DATE_ADD(
DATE_ADD(MAX(commandDateAffect),
INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH)
),
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
NULL,NULL,NULL,NULL,NULL,NULL
FROM resultWithDiff;
END$$
DELIMITER ;

View file

@ -0,0 +1,138 @@
-- ====================================================================
-- Fix GetProfileSalaryLevel to use calendar arithmetic
-- This changes the years/months/days calculation from fixed formulas
-- to actual calendar arithmetic, matching calculateGovAge behavior
-- ====================================================================
DELIMITER $$
DROP PROCEDURE IF EXISTS `GetProfileSalaryLevel`$$
CREATE DEFINER=`root`@`%` PROCEDURE `GetProfileSalaryLevel`(
IN personId VARCHAR(36),
IN _date DATE
)
BEGIN
WITH ordered AS (
SELECT * FROM profileSalary WHERE profileId = personId AND commandCode IN ('0','1','2','3','4','8','9','10','11','12','13','14','15','16','20')
),
work_session AS (
SELECT *,
COALESCE(
SUM(CASE WHEN commandCode IN (12,15,16) THEN 1 ELSE 0 END)
OVER (ORDER BY commandDateAffect, commandDateSign
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),
0) AS sessionId
FROM ordered
),
session_end AS (
SELECT sessionId, MAX(commandDateAffect) AS sessionEndDate
FROM work_session
GROUP BY sessionId
),
level_change AS (
SELECT *,
CASE
WHEN LAG(positionLevel) OVER (ORDER BY commandDateAffect, commandDateSign) = positionLevel
AND LAG(positionType) OVER (ORDER BY commandDateAffect, commandDateSign) = positionType
AND LAG(positionCee) OVER (ORDER BY commandDateAffect, commandDateSign) = positionCee
AND LAG(sessionId) OVER (ORDER BY commandDateAffect, commandDateSign) = sessionId
THEN 0
ELSE 1
END AS isNewLevel
FROM work_session
),
level_group AS (
SELECT *,
SUM(isNewLevel) OVER (ORDER BY commandDateAffect, commandDateSign) AS levelGroup
FROM level_change
),
first_rows AS (
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY levelGroup ORDER BY commandDateAffect, commandDateSign) AS rnLevel
FROM level_group
) t WHERE rnLevel = 1
),
rows_with_duration AS (
SELECT
fr.*,
LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) AS nextSessionId,
CASE
WHEN LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) IS NULL
THEN NULL
WHEN LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) <> fr.sessionId
THEN TIMESTAMPDIFF(DAY, fr.commandDateAffect, se.sessionEndDate) + 1
ELSE
TIMESTAMPDIFF(DAY, fr.commandDateAffect,
LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign))
END AS duration_days
FROM first_rows fr
LEFT JOIN session_end se ON se.sessionId = fr.sessionId
),
resultWithDiff AS (
SELECT
*,
LAG(duration_days) OVER (ORDER BY commandDateAffect, commandDateSign) AS days_diff
FROM rows_with_duration
)
SELECT
r.commandDateAffect,
r.positionType,
r.positionLevel,
r.positionCee,
r.days_diff,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect)
ELSE 0
END AS Years,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12
ELSE 0
END AS Months,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
DATEDIFF(r.commandDateAffect,
DATE_ADD(
DATE_ADD(LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign),
INTERVAL TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12 MONTH)
)
ELSE 0
END AS Days,
r.posNo,
r.positionExecutive,
r.orgRoot,
r.orgChild1,
r.orgChild2,
r.orgChild3,
r.orgChild4,
r.commandCode,
r.commandName,
r.commandNo,
r.commandYear,
r.remark
FROM resultWithDiff r
UNION ALL
SELECT
_date, NULL, NULL, NULL,
TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1,
TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date),
TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12,
DATEDIFF(_date,
DATE_ADD(
DATE_ADD(MAX(commandDateAffect),
INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH)
),
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
NULL,NULL,NULL,NULL
FROM resultWithDiff;
END$$
DELIMITER ;

View file

@ -0,0 +1,144 @@
-- ====================================================================
-- Fix GetProfileSalaryPosition to use calendar arithmetic
-- This changes the years/months/days calculation from fixed formulas
-- to actual calendar arithmetic, matching calculateGovAge behavior
-- ====================================================================
DELIMITER $$
DROP PROCEDURE IF EXISTS `GetProfileSalaryPosition`$$
CREATE DEFINER=`root`@`%` PROCEDURE `GetProfileSalaryPosition`(
IN personId VARCHAR(36),
IN _date DATE
)
BEGIN
WITH ordered AS (
SELECT * FROM profileSalary WHERE profileId = personId AND commandCode IN ('0','1','2','3','4','8','9','10','11','12','13','14','15','16','20')
),
work_session AS (
SELECT *,
COALESCE(
SUM(CASE WHEN commandCode IN (12,15,16) THEN 1 ELSE 0 END)
OVER (ORDER BY commandDateAffect, commandDateSign
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),
0) AS sessionId
FROM ordered
),
session_end AS (
SELECT sessionId, MAX(commandDateAffect) AS sessionEndDate
FROM work_session
GROUP BY sessionId
),
position_change AS (
SELECT *,
CASE
WHEN LAG(positionName) OVER (ORDER BY commandDateAffect, commandDateSign) = positionName
AND LAG(sessionId) OVER (ORDER BY commandDateAffect, commandDateSign) = sessionId
THEN 0
ELSE 1
END AS isNewPosition
FROM work_session
),
position_group AS (
SELECT *,
SUM(isNewPosition) OVER (ORDER BY commandDateAffect, commandDateSign) AS posGroup
FROM position_change
),
first_rows AS (
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY posGroup ORDER BY commandDateAffect, commandDateSign) AS rnPos
FROM position_group
) t WHERE rnPos = 1
),
rows_with_duration AS (
SELECT
fr.*,
LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) AS nextSessionId,
CASE
WHEN LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) IS NULL
THEN NULL
WHEN LEAD(fr.sessionId) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign) <> fr.sessionId
THEN TIMESTAMPDIFF(DAY, fr.commandDateAffect, se.sessionEndDate) + 1
ELSE
TIMESTAMPDIFF(DAY, fr.commandDateAffect,
LEAD(fr.commandDateAffect) OVER (ORDER BY fr.commandDateAffect, fr.commandDateSign))
END AS duration_days
FROM first_rows fr
LEFT JOIN session_end se ON se.sessionId = fr.sessionId
),
resultWithDiff AS (
SELECT
*,
LAG(duration_days) OVER (ORDER BY commandDateAffect, commandDateSign) AS days_diff
FROM rows_with_duration
)
-- ✅ NEW: Use calendar arithmetic for years/months/days calculation
SELECT
r.commandDateAffect,
r.positionName,
r.positionCee,
r.days_diff,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect)
ELSE 0
END AS Years,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12
ELSE 0
END AS Months,
CASE
WHEN LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign) IS NOT NULL THEN
TIMESTAMPDIFF(DAY,
DATE_ADD(
DATE_ADD(LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign),
INTERVAL TIMESTAMPDIFF(YEAR, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, LAG(r.commandDateAffect) OVER (ORDER BY r.commandDateAffect, r.commandDateSign), r.commandDateAffect) % 12 MONTH),
r.commandDateAffect)
ELSE 0
END AS Days,
r.posNo,
r.positionExecutive,
r.positionType,
r.positionLevel,
r.orgRoot,
r.orgChild1,
r.orgChild2,
r.orgChild3,
r.orgChild4,
r.commandCode,
r.commandName,
r.commandNo,
r.commandYear,
r.remark
FROM resultWithDiff r
UNION ALL
-- ✅ NEW: Use calendar arithmetic for the final row too
SELECT
_date, NULL, NULL,
TIMESTAMPDIFF(DAY, MAX(commandDateAffect), _date) + 1,
TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date),
TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12,
DATEDIFF(_date,
DATE_ADD(
DATE_ADD(MAX(commandDateAffect),
INTERVAL TIMESTAMPDIFF(YEAR, MAX(commandDateAffect), _date) YEAR),
INTERVAL TIMESTAMPDIFF(MONTH, MAX(commandDateAffect), _date) % 12 MONTH)
),
NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
NULL,NULL,NULL,NULL,NULL
FROM resultWithDiff;
END$$
DELIMITER ;
-- ====================================================================
-- Verification query (optional)
-- ====================================================================
-- CALL GetProfileSalaryPosition('your-profile-id', '2024-06-14');

View file

@ -20,6 +20,12 @@ import { In } from "typeorm";
import { RequestWithUser } from "../middlewares/user";
import { ApiName } from "../entities/ApiName";
import { ApiHistory } from "../entities/ApiHistory";
import { OrgRoot } from "../entities/OrgRoot";
import { OrgChild1 } from "../entities/OrgChild1";
import { OrgChild2 } from "../entities/OrgChild2";
import { OrgChild3 } from "../entities/OrgChild3";
import { OrgChild4 } from "../entities/OrgChild4";
import { OrgRevision } from "../entities/OrgRevision";
const jwt = require("jsonwebtoken");
@Route("api/v1/org/apiKey")
@ -33,6 +39,12 @@ export class ApiKeyController extends Controller {
private apiKeyRepository = AppDataSource.getRepository(ApiKey);
private apiNameRepository = AppDataSource.getRepository(ApiName);
private apiHistoryRepository = AppDataSource.getRepository(ApiHistory);
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
private orgChild1Repository = AppDataSource.getRepository(OrgChild1);
private orgChild2Repository = AppDataSource.getRepository(OrgChild2);
private orgChild3Repository = AppDataSource.getRepository(OrgChild3);
private orgChild4Repository = AppDataSource.getRepository(OrgChild4);
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
/**
* API JWT token
@ -151,6 +163,9 @@ export class ApiKeyController extends Controller {
relations: ["apiNames", "apiHistorys"],
order: { createdAt: "DESC", apiNames: { createdAt: "DESC" } },
});
const orgNames = await this.buildOrgNameBatch(apiKey);
const data = apiKey.map((_data) => ({
id: _data.id,
createdAt: _data.createdAt,
@ -163,6 +178,7 @@ export class ApiKeyController extends Controller {
dnaChild2Id: _data.dnaChild2Id,
dnaChild3Id: _data.dnaChild3Id,
dnaChild4Id: _data.dnaChild4Id,
orgName: orgNames.get(_data.id),
apiNames: _data.apiNames.map((x) => ({
id: x.id,
name: x.name,
@ -174,10 +190,139 @@ export class ApiKeyController extends Controller {
return new HttpSuccess(data);
}
private async buildOrgNameBatch(apiKeys: ApiKey[]): Promise<Map<string, string | null>> {
const currentRevision = await this.orgRevisionRepository.findOne({
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
if (!currentRevision) {
return new Map(apiKeys.map((k) => [k.id, null]));
}
const currentRevisionId = currentRevision.id;
const rootIds = [...new Set(apiKeys.map((k) => k.dnaRootId).filter(Boolean))];
const child1Ids = [...new Set(apiKeys.map((k) => k.dnaChild1Id).filter(Boolean))];
const child2Ids = [...new Set(apiKeys.map((k) => k.dnaChild2Id).filter(Boolean))];
const child3Ids = [...new Set(apiKeys.map((k) => k.dnaChild3Id).filter(Boolean))];
const child4Ids = [...new Set(apiKeys.map((k) => k.dnaChild4Id).filter(Boolean))];
const [roots, child1s, child2s, child3s, child4s] = await Promise.all([
rootIds.length > 0
? this.orgRootRepository.find({
where: [
{ id: In(rootIds), orgRevisionId: currentRevisionId },
{ ancestorDNA: In(rootIds), orgRevisionId: currentRevisionId },
],
select: ["id", "ancestorDNA", "orgRootName"],
})
: [],
child1Ids.length > 0
? this.orgChild1Repository.find({
where: [
{ id: In(child1Ids), orgRevisionId: currentRevisionId },
{ ancestorDNA: In(child1Ids), orgRevisionId: currentRevisionId },
],
select: ["id", "ancestorDNA", "orgChild1Name"],
})
: [],
child2Ids.length > 0
? this.orgChild2Repository.find({
where: [
{ id: In(child2Ids), orgRevisionId: currentRevisionId },
{ ancestorDNA: In(child2Ids), orgRevisionId: currentRevisionId },
],
select: ["id", "ancestorDNA", "orgChild2Name"],
})
: [],
child3Ids.length > 0
? this.orgChild3Repository.find({
where: [
{ id: In(child3Ids), orgRevisionId: currentRevisionId },
{ ancestorDNA: In(child3Ids), orgRevisionId: currentRevisionId },
],
select: ["id", "ancestorDNA", "orgChild3Name"],
})
: [],
child4Ids.length > 0
? this.orgChild4Repository.find({
where: [
{ id: In(child4Ids), orgRevisionId: currentRevisionId },
{ ancestorDNA: In(child4Ids), orgRevisionId: currentRevisionId },
],
select: ["id", "ancestorDNA", "orgChild4Name"],
})
: [],
]);
const rootMap = new Map(
roots.map((r) => [r.id, { name: r.orgRootName, ancestorDNA: r.ancestorDNA }]),
);
const child1Map = new Map(
child1s.map((c) => [c.id, { name: c.orgChild1Name, ancestorDNA: c.ancestorDNA }]),
);
const child2Map = new Map(
child2s.map((c) => [c.id, { name: c.orgChild2Name, ancestorDNA: c.ancestorDNA }]),
);
const child3Map = new Map(
child3s.map((c) => [c.id, { name: c.orgChild3Name, ancestorDNA: c.ancestorDNA }]),
);
const child4Map = new Map(
child4s.map((c) => [c.id, { name: c.orgChild4Name, ancestorDNA: c.ancestorDNA }]),
);
const result = new Map<string, string | null>();
for (const apiKey of apiKeys) {
if (apiKey.accessType === "ALL") {
result.set(apiKey.id, null);
continue;
}
const parts: string[] = [];
const getOrgName = (
dnaId: string,
orgMap: Map<string, { name: string; ancestorDNA: string }>,
): string | null => {
const byId = orgMap.get(dnaId);
if (byId) return byId.name;
for (const [, value] of orgMap) {
if (value.ancestorDNA === dnaId) return value.name;
}
return null;
};
if (apiKey.dnaChild4Id) {
const name = getOrgName(apiKey.dnaChild4Id, child4Map);
if (name) parts.push(name);
}
if (apiKey.dnaChild3Id) {
const name = getOrgName(apiKey.dnaChild3Id, child3Map);
if (name) parts.push(name);
}
if (apiKey.dnaChild2Id) {
const name = getOrgName(apiKey.dnaChild2Id, child2Map);
if (name) parts.push(name);
}
if (apiKey.dnaChild1Id) {
const name = getOrgName(apiKey.dnaChild1Id, child1Map);
if (name) parts.push(name);
}
if (apiKey.dnaRootId) {
const name = getOrgName(apiKey.dnaRootId, rootMap);
if (name) parts.push(name);
}
result.set(apiKey.id, parts.length > 0 ? parts.join(" ") : null);
}
return result;
}
/**
* API Api Key
* API Api Name
*
* @summary Api Key (ADMIN)
* @summary Api Name (ADMIN)
*
*/
@Get("name")

View file

@ -106,10 +106,10 @@ export class ApiManageController extends Controller {
code: "organization",
name: "ข้อมูลโครงสร้าง",
},
{
code: "position",
name: "ข้อมูลอัตรากำลัง",
},
// {
// code: "position",
// name: "ข้อมูลอัตรากำลัง",
// },
];
// รายการเอนทิตีทั้งหมด
@ -273,59 +273,240 @@ export class ApiManageController extends Controller {
description: "ข้อมูลส่วนราชการ ระดับที่ 4",
system: ["organization"],
},
{
name: "PosMaster",
repository: this.posMasterRepository,
description: "ข้อมูลอัตรากำลัง",
isMain: true,
system: ["position"],
},
{
name: "Position",
repository: this.positionRepository,
description: "ข้อมูลตำแหน่ง",
system: ["position"],
},
{
name: "OrgRoot",
repository: this.orgRootRepository,
description: "ข้อมูลหน่วยงาน",
system: ["position"],
},
{
name: "OrgChild1",
repository: this.orgChild1Repository,
description: "ข้อมูลส่วนราชการ ระดับที่ 1",
system: ["position"],
},
{
name: "OrgChild2",
repository: this.orgChild2Repository,
description: "ข้อมูลส่วนราชการ ระดับที่ 2",
system: ["position"],
},
{
name: "OrgChild3",
repository: this.orgChild3Repository,
description: "ข้อมูลส่วนราชการ ระดับที่ 3",
system: ["position"],
},
{
name: "OrgChild4",
repository: this.orgChild4Repository,
description: "ข้อมูลส่วนราชการ ระดับที่ 4",
system: ["position"],
},
{
name: "Profile",
repository: this.profileRepository,
description: "ข้อมูลคนครอง",
system: ["position"],
},
// {
// name: "PosMaster",
// repository: this.posMasterRepository,
// description: "ข้อมูลอัตรากำลัง",
// isMain: true,
// system: ["position"],
// },
// {
// name: "Position",
// repository: this.positionRepository,
// description: "ข้อมูลตำแหน่ง",
// system: ["position"],
// },
// {
// name: "OrgRoot",
// repository: this.orgRootRepository,
// description: "ข้อมูลหน่วยงาน",
// system: ["position"],
// },
// {
// name: "OrgChild1",
// repository: this.orgChild1Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 1",
// system: ["position"],
// },
// {
// name: "OrgChild2",
// repository: this.orgChild2Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 2",
// system: ["position"],
// },
// {
// name: "OrgChild3",
// repository: this.orgChild3Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 3",
// system: ["position"],
// },
// {
// name: "OrgChild4",
// repository: this.orgChild4Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 4",
// system: ["position"],
// },
// {
// name: "Profile",
// repository: this.profileRepository,
// description: "ข้อมูลคนครอง",
// system: ["position"],
// },
];
private readonly DEFAULT_PAGE_SIZE = 10; // ขนาดหน้าเริ่มต้น
private readonly EXCLUDED_COLUMNS = ["createdUserId", "lastUpdateUserId"]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์
private readonly EXCLUDED_COLUMNS = [
"createdUserId",
"lastUpdateUserId",
"createdAt",
"createdFullName",
"lastUpdateFullName",
"avatarName",
"profileId",
"prefixId",
"profileEmployeeId",
"documentId",
"orgRevisionId",
"posMasterId",
"orgRootId",
"orgChild1Id",
"orgChild2Id",
"orgChild3Id",
"orgChild4Id",
"keycloak",
"commandId",
"prefixMain",
"authRoleId",
"next_holderId",
"current_holderId",
"ancestorDNA",
"leaveCommandId",
"posLevelId",
"posTypeId",
"posExecutiveId",
"registrationProvinceId",
"registrationDistrictId",
"registrationSubDistrictId",
"currentProvinceId",
"currentDistrictId",
"currentSubDistrictId",
"isDelete",
"keycloak",
"statusCheckEdit",
"privacyCheckin",
"privacyUser",
"privacyMgt",
"dutyTimeId",
"dutyTimeEffectiveDate",
"profileId",
"profileEmployeeId",
"orgRevisionId",
"rank",
"isUpload",
"isDeleted",
"isEntry",
"prefixId",
"leaveId",
"leaveTypeId",
"isDeputy",
"isCommission",
]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์
// การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity
private readonly PROFILE_FIELD_REPLACEMENTS: Record<
string,
{ propertyName: string; type: string; comment: string; joinTable: string; joinField: string }
> = {
posLevelId: {
propertyName: "posLevelName",
type: "string",
comment: "ระดับตำแหน่ง",
joinTable: "PosLevel",
joinField: "posLevelName",
},
posTypeId: {
propertyName: "posTypeName",
type: "string",
comment: "ประเภทตำแหน่ง",
joinTable: "PosType",
joinField: "posTypeName",
},
registrationProvinceId: {
propertyName: "registrationProvinceName",
type: "string",
comment: "จังหวัดตามทะเบียนบ้าน",
joinTable: "Province",
joinField: "name",
},
registrationDistrictId: {
propertyName: "registrationDistrictName",
type: "string",
comment: "เขตตามทะเบียนบ้าน",
joinTable: "District",
joinField: "name",
},
registrationSubDistrictId: {
propertyName: "registrationSubDistrictName",
type: "string",
comment: "แขวงตามทะเบียนบ้าน",
joinTable: "SubDistrict",
joinField: "name",
},
currentProvinceId: {
propertyName: "currentProvinceName",
type: "string",
comment: "จังหวัดตามปัจจุบัน",
joinTable: "Province",
joinField: "name",
},
currentDistrictId: {
propertyName: "currentDistrictName",
type: "string",
comment: "เขตตามปัจจุบัน",
joinTable: "District",
joinField: "name",
},
currentSubDistrictId: {
propertyName: "currentSubDistrictName",
type: "string",
comment: "แขวงตามปัจจุบัน",
joinTable: "SubDistrict",
joinField: "name",
},
};
// การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Position entity
private readonly POSITION_FIELD_REPLACEMENTS: Record<
string,
{ propertyName: string; type: string; comment: string; joinTable: string; joinField: string }
> = {
posTypeId: {
propertyName: "posTypeName",
type: "string",
comment: "ประเภทตำแหน่ง",
joinTable: "PosType",
joinField: "posTypeName",
},
posLevelId: {
propertyName: "posLevelName",
type: "string",
comment: "ระดับตำแหน่ง",
joinTable: "PosLevel",
joinField: "posLevelName",
},
posExecutiveId: {
propertyName: "posExecutiveName",
type: "string",
comment: "ตำแหน่งทางการบริหาร",
joinTable: "PosExecutive",
joinField: "posExecutiveName",
},
};
// การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ ProfileEmployee entity
private readonly PROFILEEMPLOYEE_FIELD_REPLACEMENTS: Record<
string,
{ propertyName: string; type: string; comment: string; joinTable: string; joinField: string }
> = {
posLevelId: {
propertyName: "posLevelName",
type: "string",
comment: "ระดับชั้นงาน",
joinTable: "EmployeePosLevel",
joinField: "posLevelName",
},
posTypeId: {
propertyName: "posTypeName",
type: "string",
comment: "กลุ่มงาน",
joinTable: "EmployeePosType",
joinField: "posTypeName",
},
};
// การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ ProfileLeave entity
private readonly PROFILELEAVE_FIELD_REPLACEMENTS: Record<
string,
{ propertyName: string; type: string; comment: string; joinTable: string; joinField: string }
> = {
leaveTypeId: {
propertyName: "leaveTypeName",
type: "string",
comment: "ประเภทการลา",
joinTable: "LeaveType",
joinField: "name",
},
};
private validateSuperAdminRole(user: any): void {
if (!user.role.includes("SUPER_ADMIN")) {
@ -364,11 +545,8 @@ export class ApiManageController extends Controller {
const result = this.entities
.filter((entity) => entity.system.includes(system))
.map(({ name, repository, description, isMain }) => ({
tb: name,
description,
isMain: isMain || false,
propertys: repository.metadata.columns
.map(({ name, repository, description, isMain }) => {
let columns = repository.metadata.columns
.filter(
(column: any) =>
!column.isPrimary && !this.EXCLUDED_COLUMNS.includes(column.propertyName),
@ -378,9 +556,115 @@ export class ApiManageController extends Controller {
type: typeof column.type === "string" ? column.type : "string",
comment: column.comment,
key: column.propertyName,
})),
}));
// Special handling for Profile entity - replace ID fields with name fields
if (name === "Profile") {
const replacementKeys = Object.keys(this.PROFILE_FIELD_REPLACEMENTS);
// Remove ID fields that should be replaced
columns = columns.filter(
(col: { propertyName: string }) => !replacementKeys.includes(col.propertyName),
);
// Add the corresponding name fields
const nameFields = replacementKeys.map((key) => ({
propertyName: this.PROFILE_FIELD_REPLACEMENTS[key].propertyName,
type: "string",
comment: this.PROFILE_FIELD_REPLACEMENTS[key].comment,
key: this.PROFILE_FIELD_REPLACEMENTS[key].propertyName,
}));
columns = [...columns, ...nameFields];
}
// Special handling for Position entity - replace ID fields with name fields
if (name === "Position") {
const replacementKeys = Object.keys(this.POSITION_FIELD_REPLACEMENTS);
// Remove ID fields that should be replaced
columns = columns.filter(
(col: { propertyName: string }) => !replacementKeys.includes(col.propertyName),
);
// Add the corresponding name fields
const nameFields = replacementKeys.map((key) => ({
propertyName: this.POSITION_FIELD_REPLACEMENTS[key].propertyName,
type: "string",
comment: this.POSITION_FIELD_REPLACEMENTS[key].comment,
key: this.POSITION_FIELD_REPLACEMENTS[key].propertyName,
}));
columns = [...columns, ...nameFields];
}
// Special handling for ProfileEmployee entity - replace ID fields with name fields
if (name === "ProfileEmployee") {
const replacementKeys = Object.keys(this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS);
// Remove ID fields that should be replaced
columns = columns.filter(
(col: { propertyName: string }) => !replacementKeys.includes(col.propertyName),
);
// Add the corresponding name fields
const nameFields = replacementKeys.map((key) => ({
propertyName: this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].propertyName,
type: "string",
comment: this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].comment,
key: this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].propertyName,
}));
columns = [...columns, ...nameFields];
}
// Special handling for ProfileLeave entity - replace ID fields with name fields
if (name === "ProfileLeave") {
const replacementKeys = Object.keys(this.PROFILELEAVE_FIELD_REPLACEMENTS);
// Remove ID fields that should be replaced
columns = columns.filter(
(col: { propertyName: string }) => !replacementKeys.includes(col.propertyName),
);
// Add the corresponding name fields
const nameFields = replacementKeys.map((key) => ({
propertyName: this.PROFILELEAVE_FIELD_REPLACEMENTS[key].propertyName,
type: "string",
comment: this.PROFILELEAVE_FIELD_REPLACEMENTS[key].comment,
key: this.PROFILELEAVE_FIELD_REPLACEMENTS[key].propertyName,
}));
columns = [...columns, ...nameFields];
}
// Special handling for PosMaster entity - add Profile fields for holder information
if (name === "PosMaster") {
// Add Profile fields that are accessible via current_holder relation
const profileFields = ["prefix", "rank", "firstName", "lastName", "citizenId"];
const profileRepository = AppDataSource.getRepository(Profile);
const profileColumns = profileRepository.metadata.columns
.filter(
(column: any) => !column.isPrimary && profileFields.includes(column.propertyName),
)
.map((column: any) => ({
propertyName: `Profile.${column.propertyName}`,
type: typeof column.type === "string" ? column.type : "string",
comment: column.comment,
key: `Profile.${column.propertyName}`,
}));
columns = [...columns, ...profileColumns];
}
return {
tb: name,
description,
isMain: isMain || false,
propertys: columns,
};
});
return new HttpSuccess(result);
} catch (error) {
throw new HttpError(

File diff suppressed because it is too large Load diff

View file

@ -123,18 +123,25 @@ export class AuthRoleController extends Controller {
// เช็คว่าถ้ามีค่า current_holderId ให้ลบ key สิทธิ์ใน redis
if (posMaster.current_holderId) {
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.del("role_" + posMaster.current_holderId, (err: Error, response: Response) => {
if (err) throw err;
redisClient.del("role_" + posMaster.current_holderId, (err: Error) => {
if (err) console.error("Redis delete role error:", err);
});
redisClient.del("menu_" + posMaster.current_holderId, (err: Error, response: Response) => {
if (err) throw err;
redisClient.del("menu_" + posMaster.current_holderId, (err: Error) => {
if (err) console.error("Redis delete menu error:", err);
});
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
return new HttpSuccess();
@ -260,13 +267,33 @@ export class AuthRoleController extends Controller {
return newAttr;
});
const before = structuredClone(record);
await Promise.all([
this.authRoleRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)),
]);
const redisClient = await this.redis.createClient({
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(AuthRole, record);
await Promise.all(
newAttrs.map((attr) => queryRunner.manager.save(AuthRoleAttr, attr))
);
await queryRunner.commitTransaction();
setLogDataDiff(req, { before, after: record });
} catch (error) {
await queryRunner.rollbackTransaction();
console.error("Error saving auth role:", error);
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาดในการบันทึกข้อมูลบทบาท กรุณาลองใหม่ในภายหลัง"
);
} finally {
await queryRunner.release();
}
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -274,6 +301,11 @@ export class AuthRoleController extends Controller {
await redisClient.flushdb(function (err: any, succeeded: any) {
console.log(succeeded); // will be true if successfull
});
} finally {
if (redisClient) {
redisClient.quit();
}
}
return new HttpSuccess();
}

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ import {
Path,
Request,
Response,
Get
Get,
} from "tsoa";
import { LessThan, MoreThan } from "typeorm";
import { AppDataSource } from "../database/data-source";
@ -37,9 +37,7 @@ export class CommandOperatorController extends Controller {
* @param commandId
*/
@Get("{commandId}")
async getCommandOperatorByCommandId(
@Path() commandId: string
) {
async getCommandOperatorByCommandId(@Path() commandId: string) {
const command = await this.commandRepo.findOne({
where: { id: commandId },
select: { id: true },
@ -61,10 +59,7 @@ export class CommandOperatorController extends Controller {
* @param operatorId
*/
@Get("swap/{direction}/{operatorId}")
async swapCommandOperator(
@Path() direction: string,
@Path() operatorId: string,
) {
async swapCommandOperator(@Path() direction: string, @Path() operatorId: string) {
const source = await this.commandOperatorRepo.findOne({
where: { id: operatorId },
});
@ -106,10 +101,7 @@ export class CommandOperatorController extends Controller {
source.orderNo = dest.orderNo;
dest.orderNo = temp;
await Promise.all([
this.commandOperatorRepo.save(source),
this.commandOperatorRepo.save(dest),
]);
await Promise.all([this.commandOperatorRepo.save(source), this.commandOperatorRepo.save(dest)]);
return new HttpSuccess();
}
@ -141,9 +133,7 @@ export class CommandOperatorController extends Controller {
const nextOrderNo = (lastOrderNo?.orderNo ?? 1) + 1;
const now = new Date();
const operator = Object.assign(
new CommandOperator(),
{
const operator = Object.assign(new CommandOperator(), {
...body,
commandId: command.id,
orderNo: nextOrderNo,
@ -153,8 +143,7 @@ export class CommandOperatorController extends Controller {
lastUpdateUserId: request.user.sub,
lastUpdateFullName: request.user.name,
lastUpdatedAt: now,
}
);
});
await this.commandOperatorRepo.save(operator);
return new HttpSuccess();
}
@ -166,10 +155,7 @@ export class CommandOperatorController extends Controller {
* @param operatorId
*/
@Delete("{commandId}/{operatorId}")
public async deleteCommandOperator(
@Path() commandId: string,
@Path() operatorId: string,
) {
public async deleteCommandOperator(@Path() commandId: string, @Path() operatorId: string) {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
@ -215,10 +201,9 @@ export class CommandOperatorController extends Controller {
return new HttpSuccess(true);
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
console.error("Delete command operator error:", error);
} finally {
await queryRunner.release();
}
}
}

View file

@ -0,0 +1,576 @@
import {
Controller,
Post,
Put,
Patch,
Delete,
Route,
Security,
Tags,
Body,
Path,
Request,
Response,
Get,
Query,
} from "tsoa";
import { AppDataSource } from "../database/data-source";
import HttpStatus from "../interfaces/http-status";
import HttpSuccess from "../interfaces/http-success";
import HttpStatusCode from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import { Command } from "../entities/Command";
import { Brackets, LessThan, MoreThan, Double, In, Between, IsNull, Not, Any } from "typeorm";
import { CommandType } from "../entities/CommandType";
import { Profile, CreateProfileAllFields } from "../entities/Profile";
import { RequestWithUser, RequestWithUserWebService } from "../middlewares/user";
import { OrgRevision } from "../entities/OrgRevision";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import { PosMaster } from "../entities/PosMaster";
import permission from "../interfaces/permission";
import { viewCurrentTenureOfficer } from "../entities/view/viewCurrentTenureOfficer";
import { CommandController } from "./CommandController";
import Extension from "../interfaces/extension";
import { viewRegistryOfficer } from "../entities/view/viewRegistryOfficer";
import { viewRegistryEmployee } from "../entities/view/viewRegistryEmployee";
import { Registry } from "../entities/Registry";
import { RegistryEmployee } from "../entities/RegistryEmployee";
import { TenurePositionOfficer } from "../entities/TenurePositionOfficer";
import { PosMasterAssign, PosMasterAssignDTO } from "../entities/PosMasterAssign";
import { PermissionProfile } from "../entities/PermissionProfile";
import { OrgRoot } from "../entities/OrgRoot";
import { MetaWorkflow } from "../entities/MetaWorkflow";
import { MetaState } from "../entities/MetaState";
import { MetaStateOperator } from "../entities/MetaStateOperator";
import { Workflow } from "../entities/Workflow";
import { State } from "../entities/State";
import { StateOperator } from "../entities/StateOperator";
import { StateOperatorUser } from "../entities/StateOperatorUser";
import {
commandTypePath,
calculateGovAge,
calculateAge,
calculateRetireDate,
calculateRetireLaw,
removeProfileInOrganize,
setLogDataDiff,
} from "../interfaces/utils";
import CallAPI from "../interfaces/call-api";
import { PostRetireToExprofile } from "./ExRetirementController"
import { Position } from "../entities/Position";
import { PosLevel } from "../entities/PosLevel";
import { TenureLevelOfficer } from "../entities/TenureLevelOfficer";
import { TenurePositionEmployee } from "../entities/TenurePositionEmployee";
import { TenureLevelEmployee } from "../entities/TenureLevelEmployee";
import { TenurePositionExecutiveOfficer } from "../entities/TenurePositionExecutiveOfficer";
@Route("api/v1/org/DevTest")
@Tags("DevTest")
@Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการได้ กรุณาลองใหม่ในภายหลัง",
)
export class DevTestController extends Controller {
private commandRepository = AppDataSource.getRepository(Command);
private commandTypeRepository = AppDataSource.getRepository(CommandType);
private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);
private orgRootRepo = AppDataSource.getRepository(OrgRoot);
private posMasterRepo = AppDataSource.getRepository(PosMaster);
private profileRepo = AppDataSource.getRepository(Profile);
private profileEmpRepo = AppDataSource.getRepository(ProfileEmployee);
private registryRepo = AppDataSource.getRepository(Registry);
private registryEmployeeRepo = AppDataSource.getRepository(RegistryEmployee);
private posMasterAssignRepository = AppDataSource.getRepository(PosMasterAssign);
private permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile);
private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
private metaWorkflowRepo = AppDataSource.getRepository(MetaWorkflow);
private metaStateRepo = AppDataSource.getRepository(MetaState);
private metaStateOperatorRepo = AppDataSource.getRepository(MetaStateOperator);
private workflowRepo = AppDataSource.getRepository(Workflow);
private stateRepo = AppDataSource.getRepository(State);
private stateOperatorRepo = AppDataSource.getRepository(StateOperator);
private stateOperatorUserRepo = AppDataSource.getRepository(StateOperatorUser);
private positionRepository = AppDataSource.getRepository(Position);
private positionOfficerRepo = AppDataSource.getRepository(TenurePositionOfficer);
private positionEmployeeRepo = AppDataSource.getRepository(TenurePositionEmployee);
private levelOfficerRepo = AppDataSource.getRepository(TenureLevelOfficer);
private levelEmployeeRepo = AppDataSource.getRepository(TenureLevelEmployee);
private positionExecutiveOfficerRepo = AppDataSource.getRepository(
TenurePositionExecutiveOfficer,
);
@Patch("tick-officer-registry")
public async calculateOfficerPosition(
@Request() req: RequestWithUser,
@Body()
body: {
profileIds: string[];
},
) {
console.log("1.")
/**
* ===============================
* PREPARE DATA
* ===============================
*/
const profile = await this.profileRepo.find({
where: { id: In(body.profileIds) },
relations: {
posLevel: true,
posType: true,
},
});
if (!profile.length) return;
const [{ today }] = await AppDataSource.query(
"SELECT CURRENT_DATE() as today",
);
const orgRevision = await this.orgRevisionRepo.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
/**
* ===============================
* TRANSACTION
* ===============================
*/
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
console.log("2.")
try {
/**
* ===============================
* RESULT BUFFERS (SAVE ARRAY)
* ===============================
*/
const positionOfficerBulk: any[] = [];
const levelOfficerBulk: any[] = [];
const executiveOfficerBulk: any[] = [];
console.log("3.")
/**
* ===============================
* MAIN LOOP (SINGLE LOOP)
* ===============================
*/
for (const x of profile) {
const currentDate =
x.isLeave && x.leaveDate
? Extension.toDateOnlyString(x.leaveDate)
: today;
/**
* ====================================
* PARALLEL STORED PROCEDURES
* ====================================
*/
const [
positionResult,
levelResult,
executiveResult,
] = await Promise.all([
AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [
x.id,
currentDate,
]),
AppDataSource.query("CALL GetProfileSalaryLevel(?, ?)", [
x.id,
currentDate,
]),
AppDataSource.query("CALL GetProfileSalaryExecutive(?, ?)", [
x.id,
currentDate,
]),
]);
console.log("4.",x.id)
/**
* ====================================
* POSITION
* ====================================
*/
const posRows = positionResult?.[0] ?? [];
const posMap =
posRows.length > 1
? posRows.slice(1).map((r: any, i: number) => ({
days_diff: Number(r.days_diff) || 0,
positionName: posRows[i]?.positionName,
}))
: [];
const posCal = posMap
.filter((p:any) => p.positionName === x.position)
.reduce(
(a:any, c:any) => ({
days_diff: a.days_diff + c.days_diff,
positionName: c.positionName,
}),
{ days_diff: 0, positionName: null },
);
positionOfficerBulk.push({
profileId: x.id,
positionName: posCal.positionName,
days_diff: posCal.days_diff,
Years: Math.floor(posCal.days_diff / 365.2524),
Months: Math.floor((posCal.days_diff / 30.4375) % 12),
Days: Math.floor(posCal.days_diff % 30.4375),
});
console.log("5.",x.id)
/**
* ====================================
* 2 POSITION LEVEL
* ====================================
*/
const lvlRows = levelResult?.[0] ?? [];
const lvlMap =
lvlRows.length > 1
? lvlRows.slice(1).map((r: any, i: number) => ({
days_diff: Number(r.days_diff) || 0,
positionType: lvlRows[i]?.positionType,
positionLevel: lvlRows[i]?.positionLevel,
positionCee: lvlRows[i]?.positionCee,
}))
: [];
const lvlCal = lvlMap
.filter(
(l:any) =>
l.positionLevel === x.posLevel?.posLevelName &&
l.positionType === x.posType?.posTypeName,
)
.reduce(
(a:any, c:any) => ({
days_diff: a.days_diff + c.days_diff,
positionType: c.positionType,
positionLevel: c.positionLevel,
positionCee: c.positionCee,
}),
{
days_diff: 0,
positionType: null,
positionLevel: null,
positionCee: null,
},
);
levelOfficerBulk.push({
profileId: x.id,
positionType: lvlCal.positionType,
positionLevel: lvlCal.positionLevel,
positionCee: lvlCal.positionCee,
days_diff: lvlCal.days_diff,
Years: x.posLevel ? (lvlCal.days_diff / 365.2524).toFixed(4) : 0,
Months: x.posLevel ? ((lvlCal.days_diff / 30.4375) % 12).toFixed(4) : 0,
Days: x.posLevel ? (lvlCal.days_diff % 30.4375).toFixed(4) : 0,
});
console.log("6.",x.id)
/**
* ====================================
* 3 POSITION EXECUTIVE
* ====================================
*/
const exeRows = executiveResult?.[0] ?? [];
const exeMap =
exeRows.length > 1
? exeRows.slice(1).map((r: any, i: number) => ({
days_diff: Number(r.days_diff) || 0,
positionExecutive: exeRows[i]?.positionExecutive,
}))
: [];
const position = await this.positionRepository.findOne({
where: {
positionIsSelected: true,
posMaster: {
orgRevisionId: orgRevision?.id,
current_holderId: x.id,
},
},
order: { createdAt: "DESC" },
relations: {
posExecutive: true,
},
});
const exeName = position?.posExecutive?.posExecutiveName;
const exeCal = exeMap
.filter((e:any) => exeName && e.positionExecutive === exeName)
.reduce(
(a:any, c:any) => ({
days_diff: a.days_diff + c.days_diff,
positionExecutive: c.positionExecutive,
}),
{ days_diff: 0, positionExecutive: null },
);
executiveOfficerBulk.push({
profileId: x.id,
positionExecutiveName: exeCal.positionExecutive,
days_diff: exeCal.days_diff,
Years: (exeCal.days_diff / 365.2524).toFixed(4),
Months: ((exeCal.days_diff / 30.4375) % 12).toFixed(4),
Days: (exeCal.days_diff % 30.4375).toFixed(4),
});
}
console.log("7.")
/**
* ===============================
* CLEAR ALL DATA AND SAVE ARRAY (BULK)
* ===============================
*/
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(this.positionOfficerRepo.target)
.execute();
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(this.levelOfficerRepo.target)
.execute();
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(this.positionExecutiveOfficerRepo.target)
.execute();
console.log("8.")
await queryRunner.manager.save(this.positionOfficerRepo.target, positionOfficerBulk);
await queryRunner.manager.save(this.levelOfficerRepo.target, levelOfficerBulk);
await queryRunner.manager.save(this.positionExecutiveOfficerRepo.target,executiveOfficerBulk);
console.log("9.")
/**
* ===============================
* REGISTRY OFFICER (SYNC VIEW)
* ===============================
*/
const allRegis = await queryRunner.manager
.getRepository(viewRegistryOfficer)
.createQueryBuilder("registryOfficer")
.where("registryOfficer.profileId IN (:...profileIds)", {
profileIds: new Set(profile.map((p) => p.id))
})
.getMany();
const mapRegistryData = allRegis.map((x) => ({
...x,
isProbation: Boolean(x.isProbation),
isLeave: Boolean(x.isLeave),
isRetirement: Boolean(x.isRetirement),
Educations: x.Educations ? JSON.stringify(x.Educations) : "",
}));
console.log("10.")
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(this.registryRepo.target)
.execute();
if (mapRegistryData.length > 0) {
await queryRunner.manager.save(this.registryRepo.target, mapRegistryData);
}
console.log("11.")
/**
* ===============================
* COMMIT
* ===============================
*/
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
@Post("getDNA")
public async GetData(
@Request() req: RequestWithUser
){
let _data: any = {
root: null,
child1: null,
child2: null,
child3: null,
child4: null,
};
_data = await new permission().PermissionOrgList(req, "COMMAND");
return new HttpSuccess(_data);
}
@Post("calculateGovAge")
public async calculateGovAge(
@Request() req: RequestWithUser,
@Body()
body: {
profileId: string;
},
){
return new HttpSuccess(await calculateGovAge(body.profileId, "OFFICER"));
}
/**
* @summary Test Job 2
*/
@Post("cronjobCommand")
async CronjobCommand() {
const commandController = new CommandController();
await commandController.cronjobCommand();
}
/**
* @summary payload & Endpoint
*/
@Put("path-excec/{id}")
async Bright(
@Path() id: string,
@Request() request: RequestWithUser,
) {
const command = await this.commandRepository.findOne({
where: { id: id },
relations: ["commandType", "commandRecives", "commandSends", "commandSends.commandSendCCs"],
});
if (!command) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลคำสั่งนี้");
}
const path = commandTypePath(command.commandType.code);
return new HttpSuccess({
path: path + "/excecute",
refIds: command.commandRecives
.filter((x) => x.refId != null)
.map((x) => ({
refId: x.refId,
commandNo: command.commandNo,
commandYear: command.commandYear,
commandId: command.id,
remark: command.positionDetail,
amount: x.amount,
amountSpecial: x.amountSpecial,
positionSalaryAmount: x.positionSalaryAmount,
mouthSalaryAmount: x.mouthSalaryAmount,
commandCode: command.commandType.commandCode,
commandName: command.commandType.name,
commandDateAffect: command.commandExcecuteDate,
commandDateSign: command.commandAffectDate,
})),
});
}
/**
* API tab4
* @summary API tab4
* @param {string} id Id
* @param {string} profileId profileId
*/
@Get("tab4/attachment/{id}/{profileId}")
async GetByIdTab4Attachment(
@Path() id: string,
@Path() profileId: string,
@Request() request: RequestWithUser
) {
await new permission().PermissionGet(request, "COMMAND");
let profile: Profile | ProfileEmployee | null = null;
profile = await this.profileRepo.findOne({ where: { id: profileId } });
if (!profile) {
profile = await this.profileEmpRepo.findOne({ where: { id: profileId } });
if (!profile)
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลบุคคลากรนี้");
}
const command = await this.commandRepository.findOne({
where: { id },
relations: ["commandType", "commandRecives"],
});
if (!command) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลคำสั่งนี้");
}
let _command: any = [];
const path = commandTypePath(command.commandType.code);
if (path == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ");
await new CallAPI()
.PostData(request, path + "/attachment", {
refIds: command.commandRecives
.filter((x) =>
x.refId != null &&
x.profileId != null && x.profileId == profileId
)
.map((x) => ({
refId: x.refId,
Sequence: x.order,
CitizenId: x.citizenId,
Prefix: x.prefix,
FirstName: x.firstName,
LastName: x.lastName,
Amount: x.amount,
PositionSalaryAmount: x.positionSalaryAmount,
MouthSalaryAmount: x.mouthSalaryAmount,
RemarkHorizontal: x.remarkHorizontal,
RemarkVertical: x.remarkVertical,
CommandYear: command.commandYear,
CommandExcecuteDate: command.commandExcecuteDate,
})),
})
.then(async (res) => {
_command = res;
})
.catch(() => {});
let issue =
command.isBangkok == "OFFICE"
? "สำนักปลัดกรุงเทพมหานคร"
: command.isBangkok == "BANGKOK"
? "กรุงเทพมหานคร"
: null;
if (issue == null) {
const orgRevisionActive = await this.orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
relations: ["posMasters", "posMasters.orgRoot"],
});
if (orgRevisionActive != null) {
const profile = await this.profileRepo.findOne({
where: {
keycloak: command.createdUserId.toString(),
},
});
if (profile != null) {
issue =
orgRevisionActive?.posMasters?.filter((x) => x.current_holderId == profile.id)[0]
?.orgRoot?.orgRootName || null;
}
}
}
if (issue == null) issue = "...................................";
return new HttpSuccess({
template: command.commandType.fileAttachment,
reportName: "xlsx-report",
data: {
data: _command,
issuerOrganizationName: issue,
commandNo: command.commandNo == null ? "" : Extension.ToThaiNumber(command.commandNo),
commandYear:
command.commandYear == null
? ""
: Extension.ToThaiNumber(Extension.ToThaiYear(command.commandYear).toString()),
commandExcecuteDate:
command.commandExcecuteDate == null
? ""
: Extension.ToThaiNumber(Extension.ToThaiFullDate2(command.commandExcecuteDate)),
},
});
}
}

View file

@ -1058,11 +1058,11 @@ export class EmployeePositionController extends Controller {
let checkChildConditions: any = {};
let keywordAsInt: any;
let searchShortName = "1=1";
let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`;
let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`;
let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`;
let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`;
let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`;
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let _data = await new permission().PermissionOrgList(request, "SYS_ORG_EMP");
if (body.type === 0) {
typeCondition = {
@ -1072,7 +1072,7 @@ export class EmployeePositionController extends Controller {
checkChildConditions = {
orgChild1Id: IsNull(),
};
searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 1) {
@ -1083,7 +1083,7 @@ export class EmployeePositionController extends Controller {
checkChildConditions = {
orgChild2Id: IsNull(),
};
searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 2) {
@ -1094,7 +1094,7 @@ export class EmployeePositionController extends Controller {
checkChildConditions = {
orgChild3Id: IsNull(),
};
searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 3) {
@ -1105,14 +1105,14 @@ export class EmployeePositionController extends Controller {
checkChildConditions = {
orgChild4Id: IsNull(),
};
searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 4) {
typeCondition = {
orgChild4Id: body.id,
};
searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
let findPosition: any;
let masterId = new Array();
@ -1140,10 +1140,8 @@ export class EmployeePositionController extends Controller {
select: ["posMasterId"],
});
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null;
masterId = [...new Set(masterId)];
}
@ -1158,7 +1156,7 @@ export class EmployeePositionController extends Controller {
...(body.keyword &&
(masterId.length > 0
? { id: In(masterId) }
: { posMasterNo: Like(`%${body.keyword}%`) })),
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })),
},
];

View file

@ -777,11 +777,11 @@ export class EmployeeTempPositionController extends Controller {
let checkChildConditions: any = {};
let keywordAsInt: any;
let searchShortName = "1=1";
let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`;
let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`;
let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`;
let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`;
let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`;
let searchShortName0 = `CONCAT(orgRoot.orgRootShortName,' ',posMaster.posMasterNo)`;
let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName,' ',posMaster.posMasterNo)`;
let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName,' ',posMaster.posMasterNo)`;
let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName,' ',posMaster.posMasterNo)`;
let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName,' ',posMaster.posMasterNo)`;
let _data = await new permission().PermissionOrgList(request, "SYS_ORG_TEMP");
if (body.type === 0) {
typeCondition = {
@ -791,7 +791,7 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = {
orgChild1Id: IsNull(),
};
searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT(orgRoot.orgRootShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 1) {
@ -802,7 +802,7 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = {
orgChild2Id: IsNull(),
};
searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT(orgChild1.orgChild1ShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 2) {
@ -813,7 +813,7 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = {
orgChild3Id: IsNull(),
};
searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT(orgChild2.orgChild2ShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 3) {
@ -824,14 +824,14 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = {
orgChild4Id: IsNull(),
};
searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT(orgChild3.orgChild3ShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 4) {
typeCondition = {
orgChild4Id: body.id,
};
searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT(orgChild4.orgChild4ShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`;
}
let findPosition: any;
let masterId = new Array();
@ -859,10 +859,8 @@ export class EmployeeTempPositionController extends Controller {
select: ["posMasterTempId"],
});
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterTempId));
keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null;
masterId = [...new Set(masterId)];
}
@ -877,7 +875,7 @@ export class EmployeeTempPositionController extends Controller {
...(body.keyword &&
(masterId.length > 0
? { id: In(masterId) }
: { posMasterNo: Like(`%${body.keyword}%`) })),
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })),
},
];
let query = AppDataSource.getRepository(EmployeeTempPosMaster)

View file

@ -214,6 +214,7 @@ export class OrganizationController extends Controller {
await sendToQueueOrgDraft(msg);
return new HttpSuccess("Draft is being created... Processing in the background.");
} catch (error: any) {
console.error("Error creating draft organization:", error);
throw error;
}
}
@ -2529,6 +2530,7 @@ export class OrganizationController extends Controller {
await sendToQueueOrg(msg);
return new HttpSuccess();
} catch (error: any) {
console.error("Error publishing draft organization:", error);
throw error;
}
}
@ -5807,6 +5809,7 @@ export class OrganizationController extends Controller {
.leftJoin("orgRoot.posMasters", "posMasters")
.leftJoin("posMasters.current_holder", "current_holder")
.orderBy("orgRoot.orgRootOrder", "ASC")
.addOrderBy("posMasters.posMasterOrder", "ASC")
.getMany();
const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id) || null;
@ -5847,6 +5850,7 @@ export class OrganizationController extends Controller {
.leftJoin("orgChild1.posMasters", "posMasters")
.leftJoin("posMasters.current_holder", "current_holder")
.orderBy("orgChild1.orgChild1Order", "ASC")
.addOrderBy("posMasters.posMasterOrder", "ASC")
.getMany()
: [];
@ -5888,6 +5892,7 @@ export class OrganizationController extends Controller {
.leftJoin("orgChild2.posMasters", "posMasters")
.leftJoin("posMasters.current_holder", "current_holder")
.orderBy("orgChild2.orgChild2Order", "ASC")
.addOrderBy("posMasters.posMasterOrder", "ASC")
.getMany()
: [];
@ -5929,6 +5934,7 @@ export class OrganizationController extends Controller {
.leftJoin("orgChild3.posMasters", "posMasters")
.leftJoin("posMasters.current_holder", "current_holder")
.orderBy("orgChild3.orgChild3Order", "ASC")
.addOrderBy("posMasters.posMasterOrder", "ASC")
.getMany()
: [];
@ -5965,6 +5971,7 @@ export class OrganizationController extends Controller {
.leftJoin("orgChild4.posMasters", "posMasters")
.leftJoin("posMasters.current_holder", "current_holder")
.orderBy("orgChild4.orgChild4Order", "ASC")
.addOrderBy("posMasters.posMasterOrder", "ASC")
.getMany()
: [];

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,9 @@ export class PermissionController extends Controller {
@Get("")
public async getPermission(@Request() request: RequestWithUser) {
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -91,11 +93,18 @@ export class PermissionController extends Controller {
orgRevisionId: orgRevision?.id,
},
});
if (!posMaster) {
}
// ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position)
if (!posMaster && !actingData.isAct) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
}
}
const getDetail = await this.authRoleRepo.findOne({
let getDetail: any = null;
let roleAttrData: any[] = [];
if (posMaster) {
getDetail = await this.authRoleRepo.findOne({
select: ["id", "roleName", "roleDescription"],
where: { id: posMaster.authRoleId },
});
@ -104,7 +113,7 @@ export class PermissionController extends Controller {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
const roleAttrData = await this.authRoleAttrRepo.find({
roleAttrData = await this.authRoleAttrRepo.find({
select: [
"authSysId",
"parentNode",
@ -118,6 +127,14 @@ export class PermissionController extends Controller {
],
where: { authRoleId: getDetail.id },
});
} else {
// ถ้าไม่มี posMaster แต่มี acting: สร้าง getDetail เปล่าๆ
getDetail = {
id: null,
roleName: "Acting",
roleDescription: "สิทธิ์จากตำแหน่งรักษาการ",
};
}
// ถ้า User มีตำแหน่งรักษาการ ให้รวมสิทธิ์
if (actingData.isAct && actingData.posMasterActs.length > 0) {
@ -255,6 +272,11 @@ export class PermissionController extends Controller {
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
}
return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
@Get("menu")
@ -266,7 +288,9 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true,
},
});
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -314,16 +338,22 @@ export class PermissionController extends Controller {
orgRevisionId: orgRevision?.id,
},
});
if (!posMaster) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
}
}
// ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position)
if (!posMaster && !actingData.isAct) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
}
let authRole: any = null;
let roleAttrData: any[] = [];
if (posMaster) {
if (!posMaster.authRoleId) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
}
const authRole = await this.authRoleRepo.findOne({
authRole = await this.authRoleRepo.findOne({
select: ["id"],
where: { id: posMaster.authRoleId },
});
@ -333,10 +363,11 @@ export class PermissionController extends Controller {
}
// ดึง roleAttrData ของ user ปกติ
let roleAttrData = await this.authRoleAttrRepo.find({
roleAttrData = await this.authRoleAttrRepo.find({
select: ["authSysId", "parentNode"],
where: { authRoleId: authRole.id, attrIsList: true },
});
}
// ถ้ามี acting positions ให้รวมสิทธิ์
if (actingData.isAct && actingData.posMasterActs.length > 0) {
@ -416,6 +447,11 @@ export class PermissionController extends Controller {
}
return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
/**
@ -650,7 +686,9 @@ export class PermissionController extends Controller {
@Path() system: string,
@Path() action: string,
) {
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -743,6 +781,11 @@ export class PermissionController extends Controller {
}
return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
@Get("user/{system}/{action}/{id}")
@ -759,7 +802,9 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true,
},
});
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -844,10 +889,17 @@ export class PermissionController extends Controller {
}
return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
public async getPermissionFunc(@Request() request: RequestWithUser) {
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -901,12 +953,18 @@ export class PermissionController extends Controller {
orgRevisionId: orgRevision?.id,
},
});
if (!posMaster) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
}
}
const getDetail = await this.authRoleRepo.findOne({
// ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position)
if (!posMaster && !actingData.isAct) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
}
let getDetail: any = null;
let roleAttrData: any[] = [];
if (posMaster) {
getDetail = await this.authRoleRepo.findOne({
select: ["id", "roleName", "roleDescription"],
where: { id: posMaster.authRoleId },
});
@ -914,7 +972,7 @@ export class PermissionController extends Controller {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
const roleAttrData = await this.authRoleAttrRepo.find({
roleAttrData = await this.authRoleAttrRepo.find({
select: [
"authSysId",
"parentNode",
@ -928,6 +986,14 @@ export class PermissionController extends Controller {
],
where: { authRoleId: getDetail.id },
});
} else {
// ถ้าไม่มี posMaster แต่มี acting: สร้าง getDetail เปล่าๆ
getDetail = {
id: null,
roleName: "Acting",
roleDescription: "สิทธิ์จากตำแหน่งรักษาการ",
};
}
// ถ้ามี acting positions ให้รวมสิทธิ์
if (actingData.isAct && actingData.posMasterActs.length > 0) {
@ -1054,6 +1120,11 @@ export class PermissionController extends Controller {
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
}
return reply;
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
public async Permission(req: RequestWithUser, system: string, action: string) {
@ -1079,7 +1150,9 @@ export class PermissionController extends Controller {
}
public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) {
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -1151,6 +1224,11 @@ export class PermissionController extends Controller {
redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply));
}
return reply;
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
// Helper method: ดึง org scope จากตำแหน่งปกติ
@ -1330,7 +1408,9 @@ export class PermissionController extends Controller {
@Get("checkOrg/{keycloakId}")
public async checkOrg(@Path() keycloakId: string) {
const redisClient = await this.redis.createClient({
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
@ -1412,5 +1492,10 @@ export class PermissionController extends Controller {
// }
return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
}

View file

@ -296,6 +296,7 @@ export class PosMasterActController extends Controller {
where: {
id: id,
},
relations: ["posMasterChild", "posMasterChild.current_holder"],
});
try {
result = await this.posMasterActRepository.delete({ id: id });
@ -320,6 +321,22 @@ export class PosMasterActController extends Controller {
await this.posMasterActRepository.save(p);
});
}
// ลบ Redis cache ของคนที่เป็น acting
if (posMasterAct != null && posMasterAct.posMasterChild?.current_holderId) {
const profileId = posMasterAct.posMasterChild.current_holderId;
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
const delAsync = promisify(redisClient.del).bind(redisClient);
await delAsync("role_" + profileId);
await delAsync("menu_" + profileId);
redisClient.quit();
}
return new HttpSuccess();
}

View file

@ -38,7 +38,7 @@ import { EmployeePosLevel } from "../entities/EmployeePosLevel";
import { AuthRole } from "../entities/AuthRole";
import { RequestWithUser } from "../middlewares/user";
import permission from "../interfaces/permission";
import { resolveNodeLevel, setLogDataDiff } from "../interfaces/utils";
import { resolveNodeLevel, setLogDataDiff, logPositionIsSelectedChange } from "../interfaces/utils";
import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting";
import { PosMasterAssign } from "../entities/PosMasterAssign";
import { Assign } from "../entities/Assign";
@ -1427,7 +1427,17 @@ export class PositionController extends Controller {
requestBody.positions.map(async (x: any) => {
const match = posMaster.positions.find((p: any) => p.id == x.id);
if (match) {
match.positionIsSelected = x.positionIsSelected ?? false;
const oldValue = match.positionIsSelected;
const newValue = x.positionIsSelected ?? false;
logPositionIsSelectedChange(match.id, oldValue, newValue, {
posMasterId: posMaster.id,
userId: request.user.sub,
endpoint: "updateMaster",
action: "update_position",
});
match.positionIsSelected = newValue;
match.orderNo = x.orderNo ?? null;
return match;
} else {
@ -1678,11 +1688,11 @@ export class PositionController extends Controller {
let checkChildConditions: any = {};
let keywordAsInt: any;
let searchShortName = "1=1";
let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
if (body.type != null && body.id != null) {
if (body.type === 0) {
typeCondition = {
@ -1692,7 +1702,7 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild1Id: IsNull(),
};
searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 1) {
@ -1703,7 +1713,7 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild2Id: IsNull(),
};
searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 2) {
@ -1714,7 +1724,7 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild3Id: IsNull(),
};
searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 3) {
@ -1725,14 +1735,14 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild4Id: IsNull(),
};
searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
} else {
}
} else if (body.type === 4) {
typeCondition = {
orgChild4Id: body.id,
};
searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
} else {
body.isAll = true;
@ -1777,10 +1787,8 @@ export class PositionController extends Controller {
select: ["posMasterId"],
});
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null;
masterId = [...new Set(masterId)];
//serch name สิทธิ์
@ -1813,7 +1821,7 @@ export class PositionController extends Controller {
...(body.keyword &&
(masterId.length > 0
? { id: In(masterId) }
: { posMasterNo: Like(`%${body.keyword}%`) })),
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })),
},
];
let [posMaster, total] = await AppDataSource.getRepository(PosMaster)
@ -2154,11 +2162,11 @@ export class PositionController extends Controller {
let checkChildConditions: any = {};
let keywordAsInt: any;
let searchShortName = "1=1";
let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`;
let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`;
let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`;
let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`;
let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`;
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let _data = await new permission().PermissionOrgList(request, "SYS_ORG");
if (body.type === 0) {
typeCondition = {
@ -2168,7 +2176,7 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild1Id: IsNull(),
};
searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
} else if (body.type === 1) {
typeCondition = {
@ -2178,7 +2186,7 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild2Id: IsNull(),
};
searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
} else if (body.type === 2) {
typeCondition = {
@ -2188,7 +2196,7 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild3Id: IsNull(),
};
searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
} else if (body.type === 3) {
typeCondition = {
@ -2198,13 +2206,13 @@ export class PositionController extends Controller {
checkChildConditions = {
orgChild4Id: IsNull(),
};
searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
} else if (body.type === 4) {
typeCondition = {
orgChild4Id: body.id,
};
searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
let findPosition: any;
let masterId = new Array();
@ -2241,10 +2249,8 @@ export class PositionController extends Controller {
select: ["posMasterId"],
});
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null;
masterId = [...new Set(masterId)];
}
@ -2271,7 +2277,7 @@ export class PositionController extends Controller {
...(body.keyword &&
(masterId.length > 0
? { id: In(masterId) }
: { posMasterNo: Like(`%${body.keyword}%`) })),
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })),
},
];
@ -2760,7 +2766,19 @@ export class PositionController extends Controller {
id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
}));
await this.posMasterRepository.save(sortData_0, { data: request });
// Bulk update using CASE WHEN instead of save() per row
const caseClauses_0 = sortData_0
.map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`)
.join(" ");
const ids_0 = sortData_0.map((d) => `'${d.id}'`).join(",");
await this.posMasterRepository
.createQueryBuilder()
.update(PosMaster)
.set({
posMasterOrder: () => `CASE id ${caseClauses_0} END`,
})
.where(`id IN (${ids_0})`)
.execute();
setLogDataDiff(request, { before, after: sortData_0 });
break;
}
@ -2789,7 +2807,19 @@ export class PositionController extends Controller {
id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
}));
await this.posMasterRepository.save(sortData_1, { data: request });
// Bulk update using CASE WHEN instead of save() per row
const caseClauses_1 = sortData_1
.map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`)
.join(" ");
const ids_1 = sortData_1.map((d) => `'${d.id}'`).join(",");
await this.posMasterRepository
.createQueryBuilder()
.update(PosMaster)
.set({
posMasterOrder: () => `CASE id ${caseClauses_1} END`,
})
.where(`id IN (${ids_1})`)
.execute();
setLogDataDiff(request, { before, after: sortData_1 });
break;
}
@ -2818,7 +2848,19 @@ export class PositionController extends Controller {
id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
}));
await this.posMasterRepository.save(sortData_2, { data: request });
// Bulk update using CASE WHEN instead of save() per row
const caseClauses_2 = sortData_2
.map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`)
.join(" ");
const ids_2 = sortData_2.map((d) => `'${d.id}'`).join(",");
await this.posMasterRepository
.createQueryBuilder()
.update(PosMaster)
.set({
posMasterOrder: () => `CASE id ${caseClauses_2} END`,
})
.where(`id IN (${ids_2})`)
.execute();
setLogDataDiff(request, { before, after: sortData_2 });
break;
}
@ -2847,7 +2889,19 @@ export class PositionController extends Controller {
id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
}));
await this.posMasterRepository.save(sortData_3, { data: request });
// Bulk update using CASE WHEN instead of save() per row
const caseClauses_3 = sortData_3
.map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`)
.join(" ");
const ids_3 = sortData_3.map((d) => `'${d.id}'`).join(",");
await this.posMasterRepository
.createQueryBuilder()
.update(PosMaster)
.set({
posMasterOrder: () => `CASE id ${caseClauses_3} END`,
})
.where(`id IN (${ids_3})`)
.execute();
setLogDataDiff(request, { before, after: sortData_3 });
break;
}
@ -2876,7 +2930,19 @@ export class PositionController extends Controller {
id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
}));
await this.posMasterRepository.save(sortData_4, { data: request });
// Bulk update using CASE WHEN instead of save() per row
const caseClauses_4 = sortData_4
.map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`)
.join(" ");
const ids_4 = sortData_4.map((d) => `'${d.id}'`).join(",");
await this.posMasterRepository
.createQueryBuilder()
.update(PosMaster)
.set({
posMasterOrder: () => `CASE id ${caseClauses_4} END`,
})
.where(`id IN (${ids_4})`)
.execute();
setLogDataDiff(request, { before, after: sortData_4 });
break;
}
@ -3974,7 +4040,18 @@ export class PositionController extends Controller {
statusReport: "PENDING",
});
console.log(
`[positionIsSelected-DEBUG] Deleting holder, resetting ALL positions to false (posMasterId: ${id}, userId: ${request.user.sub}, endpoint: deleteHolder)`
);
dataMaster.positions.forEach(async (position) => {
logPositionIsSelectedChange(position.id, position.positionIsSelected, false, {
posMasterId: id,
userId: request.user.sub,
endpoint: "deleteHolder",
action: "delete_holder_reset_positions",
});
await this.positionRepository.update(position.id, {
positionIsSelected: false,
});
@ -5274,11 +5351,11 @@ export class PositionController extends Controller {
let checkChildConditions: any = {};
let keywordAsInt: any;
let searchShortName = "1=1";
let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`;
let _data = await new permission().PermissionOrgList(request, "SYS_POS_CONDITION");
const orgDna = await new permission().checkDna(request, request.user.sub);
let level: any = resolveNodeLevel(orgDna);
@ -5320,7 +5397,7 @@ export class PositionController extends Controller {
// checkChildConditions = {
// orgChild1Id: IsNull(),
// };
// searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 1) {
@ -5331,7 +5408,7 @@ export class PositionController extends Controller {
// checkChildConditions = {
// orgChild2Id: IsNull(),
// };
// searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 2) {
@ -5342,7 +5419,7 @@ export class PositionController extends Controller {
// checkChildConditions = {
// orgChild3Id: IsNull(),
// };
// searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 3) {
@ -5353,14 +5430,14 @@ export class PositionController extends Controller {
// checkChildConditions = {
// orgChild4Id: IsNull(),
// };
// searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
// } else {
// }
} else if (body.type === 4) {
typeCondition = {
...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }),
};
searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`;
}
let findPosition: any;
let masterId = new Array();
@ -5397,10 +5474,8 @@ export class PositionController extends Controller {
select: ["posMasterId"],
});
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null;
masterId = [...new Set(masterId)];
}
@ -5427,7 +5502,7 @@ export class PositionController extends Controller {
...(body.keyword &&
(masterId.length > 0
? { id: In(masterId) }
: { posMasterNo: Like(`%${body.keyword}%`) })),
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })),
...(!body.isAll && { isCondition: true }),
},
];

View file

@ -25,6 +25,7 @@ import {
} from "../entities/ProfileChangeName";
import { updateName } from "../keycloak";
import permission from "../interfaces/permission";
import { updateHolderProfileHistory } from "../services/PositionService";
import { setLogDataDiff } from "../interfaces/utils";
@Route("api/v1/org/profile/changeName")
@Tags("ProfileChangeName")
@ -127,6 +128,9 @@ export class ProfileChangeNameController extends Controller {
}
}
// บันทึกประวัติคนครองตำแหน่ง (ถ้า profile นี้ครองตำแหน่งอยู่)
await updateHolderProfileHistory(profile.id, req);
return new HttpSuccess(data.id);
}

View file

@ -24,6 +24,7 @@ import {
} from "../entities/ProfileChangeName";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import permission from "../interfaces/permission";
import { updateHolderProfileHistory } from "../services/PositionService";
import { updateName } from "../keycloak";
import { setLogDataDiff } from "../interfaces/utils";
@Route("api/v1/org/profile-employee/changeName")
@ -133,6 +134,9 @@ export class ProfileChangeNameEmployeeController extends Controller {
}
}
// บันทึกประวัติคนครองตำแหน่ง (ถ้า profile นี้ครองตำแหน่งอยู่)
await updateHolderProfileHistory(profile.id, req, "EMPLOYEE");
return new HttpSuccess(data.id);
}

View file

@ -93,6 +93,7 @@ import { CreatePosMasterHistoryOfficer, getTopDegrees, getPosMasterPositions } f
import { ProfileLeaveService } from "../services/ProfileLeaveService";
// import { PostRetireToExprofile } from "./ExRetirementController";
import { getPosNumCodeSit } from "../services/CommandService";
import { updateHolderProfileHistory } from "../services/PositionService";
@Route("api/v1/org/profile")
@Tags("Profile")
@Security("bearerAuth")
@ -5774,29 +5775,26 @@ export class ProfileController extends Controller {
}
if (body.citizenId) {
const citizenIdDigits = body.citizenId.toString().split("").map(Number);
const cal =
citizenIdDigits[0] * 13 +
citizenIdDigits[1] * 12 +
citizenIdDigits[2] * 11 +
citizenIdDigits[3] * 10 +
citizenIdDigits[4] * 9 +
citizenIdDigits[5] * 8 +
citizenIdDigits[6] * 7 +
citizenIdDigits[7] * 6 +
citizenIdDigits[8] * 5 +
citizenIdDigits[9] * 4 +
citizenIdDigits[10] * 3 +
citizenIdDigits[11] * 2;
const calStp2 = cal % 11;
const chkDigit = (11 - calStp2) % 10;
if (citizenIdDigits[12] !== chkDigit) {
throw new HttpError(HttpStatus.NOT_FOUND, "ข้อมูลรหัสบัตรประจำตัวประชาชนไม่ถูกต้อง");
}
Extension.CheckCitizen(body.citizenId);
}
const record = await this.profileRepo.findOneBy({ id });
const before = structuredClone(record);
// เช็คว่ามี profileHistory ของ profile นี้หรือไม่
const historyCount = await this.profileHistoryRepo.count({
where: { profileId: id },
});
// ถ้าไม่มีเลย ให้บันทึกข้อมูลเริ่มต้น (ก่อน update) ลงไปก่อน
if (historyCount === 0) {
await this.profileHistoryRepo.save(
Object.assign(new ProfileHistory(), {
...before,
birthDateOld: before?.birthDate,
profileId: id,
id: undefined,
}),
);
}
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์นี้");
@ -5833,6 +5831,9 @@ export class ProfileController extends Controller {
}
}
// บันทึกประวัติคนครองตำแหน่ง (ถ้า profile นี้ครองตำแหน่งอยู่)
await updateHolderProfileHistory(record.id, request);
return new HttpSuccess();
}
@ -6026,11 +6027,11 @@ export class ProfileController extends Controller {
} else if (searchField == "posNo") {
queryLike = `
CASE
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo)
ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(' ', orgChild4.orgChild4ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(' ', orgChild3.orgChild3ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(' ', orgChild2.orgChild2ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(' ', orgChild1.orgChild1ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
ELSE CONCAT_WS(' ', orgRoot.orgRootShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
END LIKE :keyword
`;
}
@ -6300,7 +6301,7 @@ export class ProfileController extends Controller {
@Query() sortBy: string = "profile.dateLeave",
@Query() sort: "ASC" | "DESC" = "ASC",
) {
let _data = await new permission().PermissionOrgList(request, "SYS_REGISTRY_OFFICER");
let _data = await new permission().PermissionOrgList(request, "SYS_REGISTRY_RETIRE_OFFICER");
const { data, total } = await this.profileLeaveService.getLeaveOfficer(request, {
page,
@ -6616,11 +6617,11 @@ export class ProfileController extends Controller {
} else if (searchField == "posNo") {
queryLike = `
CASE
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo)
ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(' ', orgChild4.orgChild4ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(' ', orgChild3.orgChild3ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(' ', orgChild2.orgChild2ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(' ', orgChild1.orgChild1ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
ELSE CONCAT_WS(' ', orgRoot.orgRootShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
END LIKE :keyword
`;
}
@ -6803,18 +6804,19 @@ export class ProfileController extends Controller {
.filter(Boolean)
.join("\n");
const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
const shortName = !holder
? null
: holder.orgChild4 != null
? `${holder.orgChild4.orgChild4ShortName} ${holder.posMasterNo}`
? `${holder.orgChild4.orgChild4ShortName} ${numPart}`
: holder.orgChild3 != null
? `${holder.orgChild3.orgChild3ShortName} ${holder.posMasterNo}`
? `${holder.orgChild3.orgChild3ShortName} ${numPart}`
: holder.orgChild2 != null
? `${holder.orgChild2.orgChild2ShortName} ${holder.posMasterNo}`
? `${holder.orgChild2.orgChild2ShortName} ${numPart}`
: holder.orgChild1 != null
? `${holder.orgChild1.orgChild1ShortName} ${holder.posMasterNo}`
? `${holder.orgChild1.orgChild1ShortName} ${numPart}`
: holder.orgRoot != null
? `${holder.orgRoot.orgRootShortName} ${holder.posMasterNo}`
? `${holder.orgRoot.orgRootShortName} ${numPart}`
: null;
return {
@ -7009,11 +7011,11 @@ export class ProfileController extends Controller {
} else if (searchField == "posNo") {
queryLike = `
CASE
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo)
ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(' ', orgChild4.orgChild4ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(' ', orgChild3.orgChild3ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(' ', orgChild2.orgChild2ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(' ', orgChild1.orgChild1ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
ELSE CONCAT_WS(' ', orgRoot.orgRootShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
END LIKE :keyword
`;
}
@ -7190,7 +7192,7 @@ export class ProfileController extends Controller {
.filter(Boolean)
.join("\n");
const numPart = holder ? `${holder.posMasterNoPrefix ?? ''}${holder.posMasterNo ?? ''}${holder.posMasterNoSuffix ?? ''}` : '';
const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
const shortName = !holder
? null
@ -7949,40 +7951,38 @@ export class ProfileController extends Controller {
privacyUser: profile.privacyUser,
privacyMgt: profile.privacyMgt,
isDeputy: root?.isDeputy ?? false,
// root?.orgRootShortName && posMaster?.posMasterNo
// ? `${root?.orgRootShortName} ${posMaster?.posMasterNo}`
// : "",
};
const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
if (_profile.child4Id != null) {
_profile.node = 4;
_profile.nodeId = _profile.child4Id;
_profile.nodeDnaId = _profile.child4DnaId;
_profile.nodeShortName = _profile.child4ShortName;
_profile.posNo = `${_profile.child4ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child4ShortName} ${_numPart}`;
} else if (_profile.child3Id != null) {
_profile.node = 3;
_profile.nodeId = _profile.child3Id;
_profile.nodeDnaId = _profile.child3DnaId;
_profile.nodeShortName = _profile.child3ShortName;
_profile.posNo = `${_profile.child3ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child3ShortName} ${_numPart}`;
} else if (_profile.child2Id != null) {
_profile.node = 2;
_profile.nodeId = _profile.child2Id;
_profile.nodeDnaId = _profile.child2DnaId;
_profile.nodeShortName = _profile.child2ShortName;
_profile.posNo = `${_profile.child2ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child2ShortName} ${_numPart}`;
} else if (_profile.child1Id != null) {
_profile.node = 1;
_profile.nodeId = _profile.child1Id;
_profile.nodeDnaId = _profile.child1DnaId;
_profile.nodeShortName = _profile.child1ShortName;
_profile.posNo = `${_profile.child1ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child1ShortName} ${_numPart}`;
} else if (_profile.rootId != null) {
_profile.node = 0;
_profile.nodeId = _profile.rootId;
_profile.nodeDnaId = _profile.rootDnaId;
_profile.nodeShortName = _profile.rootShortName;
_profile.posNo = `${_profile.rootShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.rootShortName} ${_numPart}`;
}
return new HttpSuccess(_profile);
}
@ -8122,41 +8122,39 @@ export class ProfileController extends Controller {
privacyUser: profile.privacyUser,
privacyMgt: profile.privacyMgt,
isDeputy: root?.isDeputy ?? false,
// root?.orgRootShortName && posMaster?.posMasterNo
// ? `${root?.orgRootShortName} ${posMaster?.posMasterNo}`
// : "",
};
const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
if (_profile.child4Id != null) {
_profile.node = 4;
_profile.nodeId = _profile.child4Id;
_profile.nodeDnaId = _profile.child4DnaId;
_profile.nodeShortName = _profile.child4ShortName;
_profile.posNo = `${_profile.child4ShortName} ${posMaster?.posMasterNo}`;
_profile.posNo = `${_profile.child4ShortName} ${_numPart}`;
} else if (_profile.child3Id != null) {
_profile.node = 3;
_profile.nodeId = _profile.child3Id;
_profile.nodeDnaId = _profile.child3DnaId;
_profile.nodeShortName = _profile.child3ShortName;
_profile.posNo = `${_profile.child3ShortName} ${posMaster?.posMasterNo}`;
_profile.posNo = `${_profile.child3ShortName} ${_numPart}`;
} else if (_profile.child2Id != null) {
_profile.node = 2;
_profile.nodeId = _profile.child2Id;
_profile.nodeDnaId = _profile.child2DnaId;
_profile.nodeShortName = _profile.child2ShortName;
_profile.posNo = `${_profile.child2ShortName} ${posMaster?.posMasterNo}`;
_profile.posNo = `${_profile.child2ShortName} ${_numPart}`;
} else if (_profile.child1Id != null) {
_profile.node = 1;
_profile.nodeId = _profile.child1Id;
_profile.nodeDnaId = _profile.child1DnaId;
_profile.nodeShortName = _profile.child1ShortName;
_profile.posNo = `${_profile.child1ShortName} ${posMaster?.posMasterNo}`;
_profile.posNo = `${_profile.child1ShortName} ${_numPart}`;
} else if (_profile.rootId != null) {
_profile.node = 0;
_profile.nodeId = _profile.rootId;
_profile.nodeDnaId = _profile.rootDnaId;
_profile.nodeShortName = _profile.rootShortName;
_profile.posNo = `${_profile.rootShortName} ${posMaster?.posMasterNo}`;
_profile.posNo = `${_profile.rootShortName} ${_numPart}`;
}
return new HttpSuccess(_profile);
}
@ -8796,32 +8794,21 @@ export class ProfileController extends Controller {
posMasterId: posMaster?.id,
},
});
const holder = profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id);
const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
const shortName =
profile.current_holders.length == 0
holder == null
? null
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild4 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild3 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) !=
null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild2 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) !=
null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild1 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) !=
null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgRoot != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: holder.orgChild4 != null
? `${holder.orgChild4.orgChild4ShortName} ${numPart}`
: holder.orgChild3 != null
? `${holder.orgChild3.orgChild3ShortName} ${numPart}`
: holder.orgChild2 != null
? `${holder.orgChild2.orgChild2ShortName} ${numPart}`
: holder.orgChild1 != null
? `${holder.orgChild1.orgChild1ShortName} ${numPart}`
: holder.orgRoot != null
? `${holder.orgRoot.orgRootShortName} ${numPart}`
: null;
// const posMasterActs = await this.posMasterActRepository.find({
// relations: [
@ -9183,26 +9170,32 @@ export class ProfileController extends Controller {
profile.avatar && profile.avatarName ? `${profile.avatar}/${profile.avatarName}` : null,
};
const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
if (_profile.child4Id != null) {
_profile.node = 4;
_profile.nodeId = _profile.child4Id;
_profile.nodeShortName = _profile.child4ShortName;
_profile.posNo = `${_profile.child4ShortName} ${_numPart}`;
} else if (_profile.child3Id != null) {
_profile.node = 3;
_profile.nodeId = _profile.child3Id;
_profile.nodeShortName = _profile.child3ShortName;
_profile.posNo = `${_profile.child3ShortName} ${_numPart}`;
} else if (_profile.child2Id != null) {
_profile.node = 2;
_profile.nodeId = _profile.child2Id;
_profile.nodeShortName = _profile.child2ShortName;
_profile.posNo = `${_profile.child2ShortName} ${_numPart}`;
} else if (_profile.child1Id != null) {
_profile.node = 1;
_profile.nodeId = _profile.child1Id;
_profile.nodeShortName = _profile.child1ShortName;
_profile.posNo = `${_profile.child1ShortName} ${_numPart}`;
} else if (_profile.rootId != null) {
_profile.node = 0;
_profile.nodeId = _profile.rootId;
_profile.nodeShortName = _profile.rootShortName;
_profile.posNo = `${_profile.rootShortName} ${_numPart}`;
}
return new HttpSuccess(_profile);
}
@ -9332,26 +9325,32 @@ export class ProfileController extends Controller {
: "-",
};
const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
if (_profile.child4Id != null) {
_profile.node = 4;
_profile.nodeId = _profile.child4Id;
_profile.nodeShortName = _profile.child4ShortName;
_profile.posNo = `${_profile.child4ShortName} ${_numPart}`;
} else if (_profile.child3Id != null) {
_profile.node = 3;
_profile.nodeId = _profile.child3Id;
_profile.nodeShortName = _profile.child3ShortName;
_profile.posNo = `${_profile.child3ShortName} ${_numPart}`;
} else if (_profile.child2Id != null) {
_profile.node = 2;
_profile.nodeId = _profile.child2Id;
_profile.nodeShortName = _profile.child2ShortName;
_profile.posNo = `${_profile.child2ShortName} ${_numPart}`;
} else if (_profile.child1Id != null) {
_profile.node = 1;
_profile.nodeId = _profile.child1Id;
_profile.nodeShortName = _profile.child1ShortName;
_profile.posNo = `${_profile.child1ShortName} ${_numPart}`;
} else if (_profile.rootId != null) {
_profile.node = 0;
_profile.nodeId = _profile.rootId;
_profile.nodeShortName = _profile.rootShortName;
_profile.posNo = `${_profile.rootShortName} ${_numPart}`;
}
return new HttpSuccess(_profile);
}
@ -9532,38 +9531,28 @@ export class ProfileController extends Controller {
const mapDataProfile = await Promise.all(
findProfile.map(async (item: Profile) => {
const fullName = `${item.prefix}${item.firstName} ${item.lastName}`;
const shortName =
item.current_holders.length == 0
const holder = item.current_holders?.find((x) => x.orgRevisionId == findRevision.id);
const _numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
const shortName = !holder
? null
: item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 !=
null
? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 !=
null
? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
item.current_holders.find((x) => x.orgRevisionId == findRevision.id)
?.orgChild2 != null
? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
item.current_holders.find((x) => x.orgRevisionId == findRevision.id)
?.orgChild1 != null
? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: item.current_holders.find((x) => x.orgRevisionId == findRevision.id) !=
null &&
item.current_holders.find((x) => x.orgRevisionId == findRevision.id)
?.orgRoot != null
? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: holder.orgChild4 != null
? `${holder.orgChild4.orgChild4ShortName} ${_numPart}`
: holder.orgChild3 != null
? `${holder.orgChild3.orgChild3ShortName} ${_numPart}`
: holder.orgChild2 != null
? `${holder.orgChild2.orgChild2ShortName} ${_numPart}`
: holder.orgChild1 != null
? `${holder.orgChild1.orgChild1ShortName} ${_numPart}`
: holder.orgRoot != null
? `${holder.orgRoot.orgRootShortName} ${_numPart}`
: null;
const root =
item.current_holders.length == 0 ||
(item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null)
(holder != null &&
holder?.orgRoot == null)
? null
: item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot;
: holder?.orgRoot;
const rootHolder = item.current_holders?.find(
(x) => x.orgRevisionId == findRevision.id,

View file

@ -84,6 +84,7 @@ import { ProfileDuty } from "../entities/ProfileDuty";
import { CreatePosMasterHistoryEmployee, getTopDegrees } from "../services/PositionService";
import { ProfileLeaveService } from "../services/ProfileLeaveService";
import { CommandCode } from "../entities/CommandCode";
import { updateHolderProfileHistory } from "../services/PositionService";
@Route("api/v1/org/profile-employee")
@Tags("ProfileEmployee")
@Security("bearerAuth")
@ -399,24 +400,19 @@ export class ProfileEmployeeController extends Controller {
salary_raw.length > 0 && salary_raw[0].positionExecutive != null
? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].positionExecutive))
: "",
org: `${
salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-"
org: `${salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-"
? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild4)) + " "
: ""
}${
salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-"
}${salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-"
? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild3)) + " "
: ""
}${
salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-"
}${salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-"
? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild2)) + " "
: ""
}${
salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-"
}${salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-"
? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild1)) + " "
: ""
}${
salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-"
}${salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-"
? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgRoot))
: ""
}`,
@ -2386,28 +2382,27 @@ export class ProfileEmployeeController extends Controller {
}
if (body.citizenId) {
const citizenIdDigits = body.citizenId.toString().split("").map(Number);
const cal =
citizenIdDigits[0] * 13 +
citizenIdDigits[1] * 12 +
citizenIdDigits[2] * 11 +
citizenIdDigits[3] * 10 +
citizenIdDigits[4] * 9 +
citizenIdDigits[5] * 8 +
citizenIdDigits[6] * 7 +
citizenIdDigits[7] * 6 +
citizenIdDigits[8] * 5 +
citizenIdDigits[9] * 4 +
citizenIdDigits[10] * 3 +
citizenIdDigits[11] * 2;
const calStp2 = cal % 11;
const chkDigit = (11 - calStp2) % 10;
if (citizenIdDigits[12] !== chkDigit) {
throw new HttpError(HttpStatus.NOT_FOUND, "ข้อมูลรหัสบัตรประจำตัวประชาชนไม่ถูกต้อง");
}
Extension.CheckCitizen(body.citizenId);
}
const record = await this.profileRepo.findOneBy({ id });
const before = structuredClone(record);
// เช็คว่ามี profileHistory ของ profile นี้หรือไม่
const historyCount = await this.profileHistoryRepo.count({
where: { profileEmployeeId: id },
});
// ถ้าไม่มีเลย ให้บันทึกข้อมูลเริ่มต้น (ก่อน update) ลงไปก่อน
if (historyCount === 0) {
await this.profileHistoryRepo.save(
Object.assign(new ProfileEmployeeHistory(), {
...before,
birthDateOld: before?.birthDate,
profileEmployeeId: id,
id: undefined,
}),
);
}
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์นี้");
if (body.employeeClass == null || body.employeeClass == undefined || body.employeeClass == "") {
@ -2439,6 +2434,8 @@ export class ProfileEmployeeController extends Controller {
}),
);
await this.profileRepo.save(record);
// บันทึกประวัติคนครองตำแหน่ง (ถ้า profile นี้ครองตำแหน่งอยู่)
await updateHolderProfileHistory(record.id, request, "EMPLOYEE");
return new HttpSuccess();
}
@ -2853,11 +2850,11 @@ export class ProfileEmployeeController extends Controller {
} else if (searchField == "posNo") {
queryLike = `
CASE
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo)
ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(' ', orgChild4.orgChild4ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(' ', orgChild3.orgChild3ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(' ', orgChild2.orgChild2ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(' ', orgChild1.orgChild1ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
ELSE CONCAT_WS(' ', orgRoot.orgRootShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
END LIKE :keyword
`;
}
@ -3111,7 +3108,7 @@ export class ProfileEmployeeController extends Controller {
@Query() sortBy: string = "profileEmployee.dateLeave",
@Query() sort: "ASC" | "DESC" = "DESC",
) {
let _data = await new permission().PermissionOrgList(request, "SYS_REGISTRY_EMP");
let _data = await new permission().PermissionOrgList(request, "SYS_REGISTRY_RETIRE_EMP");
const { data, total } = await this.profileLeaveService.getLeaveEmployees(request, {
page,
@ -3212,11 +3209,11 @@ export class ProfileEmployeeController extends Controller {
} else if (searchField == "posNo") {
queryLike = `
CASE
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo)
ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo)
WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(' ', orgChild4.orgChild4ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(' ', orgChild3.orgChild3ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(' ', orgChild2.orgChild2ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(' ', orgChild1.orgChild1ShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
ELSE CONCAT_WS(' ', orgRoot.orgRootShortName, NULLIF(current_holders.posMasterNoPrefix,''), current_holders.posMasterNo, NULLIF(current_holders.posMasterNoSuffix,''))
END LIKE :keyword
`;
}
@ -3369,7 +3366,7 @@ export class ProfileEmployeeController extends Controller {
const data = await Promise.all(
record.map((_data) => {
const holder = _data.current_holders.find((x) => x.orgRevisionId == findRevision.id);
const numPart = holder ? `${holder.posMasterNoPrefix ?? ''}${holder.posMasterNo ?? ''}${holder.posMasterNoSuffix ?? ''}` : '';
const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
const shortName = !holder
? null
: holder.orgChild4 != null
@ -3846,7 +3843,7 @@ export class ProfileEmployeeController extends Controller {
holder.orgChild2?.orgChild2ShortName ||
holder.orgChild1?.orgChild1ShortName ||
holder.orgRoot?.orgRootShortName;
return `${shortName || ""} ${holder.posMasterNo || ""}`;
return `${shortName || ""} ${[holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}`;
});
return profile.current_holders.map((holder, index) => {
const position = holder.positions.find((position) => position.posMasterId === holder.id);
@ -4024,40 +4021,38 @@ export class ProfileEmployeeController extends Controller {
salary: profile ? profile.amount : null,
amountSpecial: profile ? profile.amountSpecial : null,
posNo: null,
// root?.orgRootShortName && posMaster?.posMasterNo
// ? `${root?.orgRootShortName} ${posMaster?.posMasterNo}`
// : "",
};
const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
if (_profile.child4Id != null) {
_profile.node = 4;
_profile.nodeId = _profile.child4Id;
_profile.nodeDnaId = _profile.child4DnaId;
_profile.nodeShortName = _profile.child4ShortName;
_profile.posNo = `${_profile.child4ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child4ShortName} ${_numPart}`;
} else if (_profile.child3Id != null) {
_profile.node = 3;
_profile.nodeId = _profile.child3Id;
_profile.nodeDnaId = _profile.child3DnaId;
_profile.nodeShortName = _profile.child3ShortName;
_profile.posNo = `${_profile.child3ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child3ShortName} ${_numPart}`;
} else if (_profile.child2Id != null) {
_profile.node = 2;
_profile.nodeId = _profile.child2Id;
_profile.nodeDnaId = _profile.child2DnaId;
_profile.nodeShortName = _profile.child2ShortName;
_profile.posNo = `${_profile.child2ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child2ShortName} ${_numPart}`;
} else if (_profile.child1Id != null) {
_profile.node = 1;
_profile.nodeId = _profile.child1Id;
_profile.nodeDnaId = _profile.child1DnaId;
_profile.nodeShortName = _profile.child1ShortName;
_profile.posNo = `${_profile.child1ShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.child1ShortName} ${_numPart}`;
} else if (_profile.rootId != null) {
_profile.node = 0;
_profile.nodeId = _profile.rootId;
_profile.nodeDnaId = _profile.rootDnaId;
_profile.nodeShortName = _profile.rootShortName;
_profile.posNo = `${_profile.rootShortName} ${_profile.posMasterNo}`;
_profile.posNo = `${_profile.rootShortName} ${_numPart}`;
}
return new HttpSuccess(_profile);
}
@ -6465,33 +6460,7 @@ export class ProfileEmployeeController extends Controller {
null
? null
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4;
const shortName =
profile.current_holders.length == 0
? null
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild4 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild3 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) !=
null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild2 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) !=
null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgChild1 != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) !=
null &&
profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)
?.orgRoot != null
? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}`
: null;
const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : '';
const _profile: any = {
profileId: profile.id,
prefix: profile.prefix,
@ -6533,7 +6502,7 @@ export class ProfileEmployeeController extends Controller {
child4ShortName: child4 == null ? null : child4.orgChild4ShortName,
node: null,
nodeId: null,
posNo: shortName,
posNo: null,
salary: profile.amount,
education:
profile && profile.profileEducations.length > 0
@ -6548,22 +6517,27 @@ export class ProfileEmployeeController extends Controller {
_profile.node = 4;
_profile.nodeId = _profile.child4Id;
_profile.nodeShortName = _profile.child4ShortName;
_profile.posNo = `${_profile.child4ShortName} ${_numPart}`;
} else if (_profile.child3Id != null) {
_profile.node = 3;
_profile.nodeId = _profile.child3Id;
_profile.nodeShortName = _profile.child3ShortName;
_profile.posNo = `${_profile.child3ShortName} ${_numPart}`;
} else if (_profile.child2Id != null) {
_profile.node = 2;
_profile.nodeId = _profile.child2Id;
_profile.nodeShortName = _profile.child2ShortName;
_profile.posNo = `${_profile.child2ShortName} ${_numPart}`;
} else if (_profile.child1Id != null) {
_profile.node = 1;
_profile.nodeId = _profile.child1Id;
_profile.nodeShortName = _profile.child1ShortName;
_profile.posNo = `${_profile.child1ShortName} ${_numPart}`;
} else if (_profile.rootId != null) {
_profile.node = 0;
_profile.nodeId = _profile.rootId;
_profile.nodeShortName = _profile.rootShortName;
_profile.posNo = `${_profile.rootShortName} ${_numPart}`;
}
return new HttpSuccess(_profile);
}

View file

@ -1001,6 +1001,24 @@ export class ProfileEmployeeTempController extends Controller {
}
const record = await this.profileRepo.findOneBy({ id });
const before = structuredClone(record);
// เช็คว่ามี profileHistory ของ profile นี้หรือไม่
const historyCount = await this.profileHistoryRepo.count({
where: { profileEmployeeId: id },
});
// ถ้าไม่มีเลย ให้บันทึกข้อมูลเริ่มต้น (ก่อน update) ลงไปก่อน
if (historyCount === 0) {
await this.profileHistoryRepo.save(
Object.assign(new ProfileEmployeeHistory(), {
...before,
birthDateOld: before?.birthDate,
profileEmployeeId: id,
id: undefined,
}),
);
}
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์นี้");
if (body.employeeClass == null || body.employeeClass == undefined || body.employeeClass == "") {

View file

@ -115,7 +115,7 @@ export class ProfileGovernmentEmployeeController extends Controller {
record.posType == null && record.posLevel == null
? null
: `${record.posType.posTypeShortName} ${record.posLevel.posLevelName}`, //ระดับ
posMasterNo: posMaster == null ? null : `${orgShortName} ${posMaster.posMasterNoPrefix ?? ''}${posMaster.posMasterNo ?? ''}${posMaster.posMasterNoSuffix ?? ''}`, //เลขที่ตำแหน่ง
posMasterNo: posMaster == null ? null : `${orgShortName} ${[posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}`, //เลขที่ตำแหน่ง
posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท
dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate),
dateRetireLaw: record.dateRetireLaw ?? null,
@ -281,7 +281,7 @@ export class ProfileGovernmentEmployeeController extends Controller {
record?.isLeave == false
? posMaster == null
? null
: `${orgShortName} ${posMaster.posMasterNoPrefix ?? ''}${posMaster.posMasterNo ?? ''}${posMaster.posMasterNoSuffix ?? ''}`
: `${orgShortName} ${[posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}`
: posNoLeave /*record && record?.profileSalary.length > 0
? `${record?.profileSalary[0].posNoAbb} ${record?.profileSalary[0].posNo}`
: null*/, //
@ -441,7 +441,7 @@ export class ProfileGovernmentEmployeeController extends Controller {
record?.isLeave == false
? posMaster == null
? null
: `${orgShortName} ${posMaster.posMasterNoPrefix ?? ''}${posMaster.posMasterNo ?? ''}${posMaster.posMasterNoSuffix ?? ''}`
: `${orgShortName} ${[posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}`
: posNoLeave /*record && record.profileSalary.length > 0
? `${record?.profileSalary[0].posNoAbb} ${record?.profileSalary[0].posNo}`
: null*/, //

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,7 @@ import { Profile } from "../entities/Profile";
import { In, LessThan, IsNull, MoreThan } from "typeorm";
import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils";
import { calculateTenure } from "../utils/tenure";
import { normalizeDurationSumSimple } from "../utils/tenure";
import { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot";
import Extension from "../interfaces/extension";
@ -161,6 +161,14 @@ export class ProfileSalaryEmployeeController extends Controller {
_position.length > 1
? _position.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0,
// Use stored procedure's calculated values (calendar arithmetic)
year:
curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0,
month:
curr.Months !== null && curr.Months !== undefined
? Math.floor(Number(curr.Months))
: 0,
day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0,
name: _position[index]?.positionName,
}))
: [];
@ -171,15 +179,25 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) {
existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else {
existing = { name: curr.name, days: curr.days };
existing = {
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing);
}
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
// Normalize the summed values using calendar arithmetic
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day);
existing.year = normalized.years;
existing.month = normalized.months;
existing.day = normalized.days;
return acc;
},
@ -195,6 +213,14 @@ export class ProfileSalaryEmployeeController extends Controller {
_posLevel.length > 1
? _posLevel.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0,
// Use stored procedure's calculated values (calendar arithmetic)
year:
curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0,
month:
curr.Months !== null && curr.Months !== undefined
? Math.floor(Number(curr.Months))
: 0,
day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0,
name:
!_posLevel[index]?.positionType && _posLevel[index]?.positionCee
? `ระดับ ${_posLevel[index]?.positionCee.trim()}`
@ -208,15 +234,25 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) {
existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else {
existing = { name: curr.name, days: curr.days };
existing = {
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing);
}
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
// Normalize the summed values using calendar arithmetic
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day);
existing.year = normalized.years;
existing.month = normalized.months;
existing.day = normalized.days;
return acc;
},
@ -254,6 +290,14 @@ export class ProfileSalaryEmployeeController extends Controller {
_position.length > 1
? _position.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0,
// Use stored procedure's calculated values (calendar arithmetic)
year:
curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0,
month:
curr.Months !== null && curr.Months !== undefined
? Math.floor(Number(curr.Months))
: 0,
day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0,
name: _position[index]?.positionName,
}))
: [];
@ -264,15 +308,25 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) {
existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else {
existing = { name: curr.name, days: curr.days };
existing = {
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing);
}
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
// Normalize the summed values using calendar arithmetic
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day);
existing.year = normalized.years;
existing.month = normalized.months;
existing.day = normalized.days;
return acc;
},
@ -288,6 +342,14 @@ export class ProfileSalaryEmployeeController extends Controller {
_posLevel.length > 1
? _posLevel.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0,
// Use stored procedure's calculated values (calendar arithmetic)
year:
curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0,
month:
curr.Months !== null && curr.Months !== undefined
? Math.floor(Number(curr.Months))
: 0,
day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0,
name:
!_posLevel[index]?.positionType && _posLevel[index]?.positionCee
? `ระดับ ${_posLevel[index]?.positionCee.trim()}`
@ -301,15 +363,25 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) {
existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else {
existing = { name: curr.name, days: curr.days };
existing = {
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing);
}
const { year, month, day } = calculateTenure(existing.days);
existing.year = year;
existing.month = month;
existing.day = day;
// Normalize the summed values using calendar arithmetic
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day);
existing.year = normalized.years;
existing.month = normalized.months;
existing.day = normalized.days;
return acc;
},

View file

@ -1791,4 +1791,56 @@ export class ProfileSalaryTempController extends Controller {
await this.salaryRepo.save(sortLevel);
return new HttpSuccess();
}
/**
* API
* @summary API
*/
@Put("sort-order")
public async reorderSalaryByCommandDate(
@Request() req: RequestWithUser,
@Body() body: { profileId: string; type: "OFFICER" | "EMPLOYEE" },
) {
const isOfficer = body.type.toUpperCase() === "OFFICER";
// Step 1: SELECT ข้อมูลตาม profileId และ type
const salaryTemps = await this.salaryRepo.find({
where: isOfficer ? { profileId: body.profileId } : { profileEmployeeId: body.profileId },
});
if (salaryTemps.length === 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งเงินเดือน");
}
// Step 2: เรียงลำดับตาม commandDateAffect (ASC)
// ถ้า commandDateAffect เท่ากัน ให้ใช้ order เดิมเป็น secondary sort
const sortedSalary = salaryTemps.sort((a, b) => {
// ถ้า commandDateAffect เป็น null ให้ถือว่าเป็นค่าน้อยสุด
const dateA = a.commandDateAffect ? new Date(a.commandDateAffect).getTime() : 0;
const dateB = b.commandDateAffect ? new Date(b.commandDateAffect).getTime() : 0;
if (dateA !== dateB) {
return dateA - dateB; // เรียงตามวันที่คำสั่งมีผล
}
// ถ้าวันที่เท่ากัน ให้ใช้ order เดิม
const orderA = a.order ?? 0;
const orderB = b.order ?? 0;
return orderA - orderB;
});
// Step 3: UPDATE ฟิลด์ order ตามการเรียงใหม่
const dateNow = new Date();
const updatedSalary = sortedSalary.map((item, index) => ({
...item,
order: index + 1,
lastUpdateUserId: req.user.sub,
lastUpdateFullName: req.user.name,
lastUpdatedAt: dateNow,
}));
await this.salaryRepo.save(updatedSalary);
return new HttpSuccess();
}
}

View file

@ -38,6 +38,10 @@ export class ScriptProfileOrgController extends Controller {
process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24",
10,
);
private readonly LEAVE_SERVICE_BATCH_SIZE = parseInt(
process.env.LEAVE_SERVICE_BATCH_SIZE || "50",
10,
);
/**
* Script to update profile's organizational structure in leave service and sync to Keycloak
@ -45,7 +49,7 @@ export class ScriptProfileOrgController extends Controller {
* @summary Update org structure for profiles updated within a certain time window and sync to Keycloak
*/
@Post("update-org")
public async cronjobUpdateOrg(@Request() request: RequestWithUser) {
public async cronjobUpdateOrg(@Request() _request: RequestWithUser) {
// Idempotency check - prevent concurrent runs
if (this.isRunning) {
console.log("cronjobUpdateOrg: Job already running, skipping this execution");
@ -176,21 +180,6 @@ export class ScriptProfileOrgController extends Controller {
});
}
// Update profile's org structure in leave service by calling API
console.log("cronjobUpdateOrg: Calling leave service API", {
payloadCount: payloads.length,
});
await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, {
headers: {
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
timeout: 30000, // 30 second timeout
});
console.log("cronjobUpdateOrg: Leave service API call successful");
// Group profile IDs by type for proper syncing
const profileIdsByType = this.groupProfileIdsByType(payloads);
@ -256,16 +245,90 @@ export class ScriptProfileOrgController extends Controller {
syncResults.failed += typeResult.failed;
}
// Update profile's org structure in leave service by calling API
console.log("cronjobUpdateOrg: Calling leave service API with chunking", {
payloadCount: payloads.length,
batchSize: this.LEAVE_SERVICE_BATCH_SIZE,
expectedBatches: Math.ceil(payloads.length / this.LEAVE_SERVICE_BATCH_SIZE),
});
const chunks = this.chunkArray(payloads, this.LEAVE_SERVICE_BATCH_SIZE);
const leaveServiceResults = {
total: payloads.length,
success: 0,
failed: 0,
batchesCompleted: 0,
batchesFailed: 0,
};
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const batchNumber = i + 1;
console.log(
`cronjobUpdateOrg: Processing leave service batch ${batchNumber}/${chunks.length}`,
{
batchSize: chunk.length,
batchRange: `${i * this.LEAVE_SERVICE_BATCH_SIZE + 1}-${Math.min(
(batchNumber + 1) * this.LEAVE_SERVICE_BATCH_SIZE,
payloads.length,
)}`,
},
);
try {
await axios.put(
`${process.env.API_URL}/leave-beginning/schedule/update-dna`,
chunk,
{
headers: {
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
timeout: 120000, // 120 second timeout per chunk
},
);
leaveServiceResults.success += chunk.length;
leaveServiceResults.batchesCompleted++;
console.log(`cronjobUpdateOrg: Leave service batch ${batchNumber}/${chunks.length} completed`, {
success: chunk.length,
});
} catch (error: any) {
leaveServiceResults.failed += chunk.length;
leaveServiceResults.batchesFailed++;
console.error(
`cronjobUpdateOrg: Leave service batch ${batchNumber}/${chunks.length} failed`,
{
error: error.message,
batchSize: chunk.length,
responseStatus: error.response?.status,
responseData: error.response?.data,
},
);
// Continue processing remaining batches
}
}
console.log("cronjobUpdateOrg: Leave service API call completed", {
...leaveServiceResults,
});
const duration = Date.now() - startTime;
console.log("cronjobUpdateOrg: Job completed", {
duration: `${duration}ms`,
processed: payloads.length,
leaveServiceResults,
syncResults,
});
return new HttpSuccess({
message: "Update org completed",
processed: payloads.length,
leaveServiceResults,
syncResults,
duration: `${duration}ms`,
});

View file

@ -1,5 +1,6 @@
import { Body, Controller, Post, Route } from "tsoa";
import { Body, Controller, Post, Request, Route, Security } from "tsoa";
import { sendWebSocket } from "../services/webSocket";
import { RequestWithUser } from "../middlewares/user";
@Route("/api/v1/org/through-socket")
export class SocketController extends Controller {
@ -22,4 +23,39 @@ export class SocketController extends Controller {
},
);
}
@Post("notify-from-token")
@Security("bearerAuth")
async notifyFromToken(
@Body()
payload: {
message: string;
targetUserId?: string | string[];
roles?: string | string[];
error?: boolean;
},
@Request() req: RequestWithUser,
) {
const toArray = (value?: string | string[]) => {
if (Array.isArray(value)) return value.filter(Boolean);
if (typeof value === "string" && value.trim()) return [value];
return [] as string[];
};
const targetUserIds = toArray(payload.targetUserId);
const targetRoles = toArray(payload.roles);
// If caller provides explicit user targets, do not combine with role targeting.
// This prevents accidental broad notifications when roles include common roles.
const recipients =
targetUserIds.length > 0
? { userId: targetUserIds, roles: [] as string[] }
: { userId: [req.user.sub], roles: targetRoles };
sendWebSocket(
"socket-notification",
{ success: !payload.error, message: payload.message },
recipients,
);
}
}

View file

@ -580,18 +580,27 @@ export class KeycloakController extends Controller {
new Brackets((qb) => {
qb.orWhere(
body.keyword != null && body.keyword != ""
? `profile.citizenId like '%${body.keyword}%'`
? `profile.citizenId LIKE :keyword`
: "1=1",
{
keyword: `%${body.keyword}%`,
}
)
.orWhere(
body.keyword != null && body.keyword != ""
? `profile.email like '%${body.keyword}%'`
? `profile.email LIKE :keyword`
: "1=1",
{
keyword: `%${body.keyword}%`,
}
)
.orWhere(
body.keyword != null && body.keyword != ""
? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) like '%${body.keyword}%'`
? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) LIKE :keyword`
: "1=1",
{
keyword: `%${body.keyword}%`,
}
);
}),
)
@ -625,18 +634,27 @@ export class KeycloakController extends Controller {
new Brackets((qb) => {
qb.orWhere(
body.keyword != null && body.keyword != ""
? `profileEmployee.citizenId like '%${body.keyword}%'`
? `profileEmployee.citizenId LIKE :keyword`
: "1=1",
{
keyword: `%${body.keyword}%`,
}
)
.orWhere(
body.keyword != null && body.keyword != ""
? `profileEmployee.email like '%${body.keyword}%'`
? `profileEmployee.email LIKE :keyword`
: "1=1",
{
keyword: `%${body.keyword}%`,
}
)
.orWhere(
body.keyword != null && body.keyword != ""
? `CONCAT(profileEmployee.prefix, profileEmployee.firstName," ",profileEmployee.lastName) like '%${body.keyword}%'`
? `CONCAT(profileEmployee.prefix, profileEmployee.firstName," ",profileEmployee.lastName) LIKE :keyword`
: "1=1",
{
keyword: `%${body.keyword}%`,
}
);
}),
)

View file

@ -1088,11 +1088,10 @@ export class WorkflowController extends Controller {
if (body.isAct) {
// ตำแหน่งของคนที่เลือกไปรักษาการ
let childPosition = "";
if (x.posType === "อำนวยการ" || x.posType === "บริหาร") {
childPosition = x.posExecutiveName || "";
if (!childPosition) {
childPosition = `${x.position || ""}ระดับ${x.posLevel || ""}`.trim();
}
if (x.positionSignChild) {
childPosition = x.positionSignChild;
} else if (x.posExecutiveName) {
childPosition = x.posExecutiveName;
} else {
childPosition = `${x.position || ""}${x.posLevel || ""}`.trim();
}

View file

@ -34,6 +34,14 @@ export class Command extends EntityBase {
})
issue: string;
@Column({
nullable: true,
comment: "ชื่อย่อหน่วยงานที่ออกคำสั่ง",
length: 16,
default: null,
})
shortName: string;
@Column({
nullable: true,
comment: "เลขที่คำสั่ง",

View file

@ -99,51 +99,51 @@ export class PosMasterEmployeeHistory extends EntityBase {
})
ancestorDNA: string;
// @Column({
// nullable: true,
// length: 40,
// comment: "คีย์นอก(FK)ของตาราง profile",
// default: null,
// })
// profileId: string;
@Column({
nullable: true,
length: 40,
comment: "คีย์นอก(FK)ของตาราง profileEmployee",
default: null,
})
profileEmployeeId: string;
// @Column({
// nullable: true,
// length: 40,
// comment: "dna ของตาราง orgRoot",
// default: null,
// })
// rootDnaId: string;
@Column({
nullable: true,
length: 40,
comment: "dna ของตาราง orgRoot",
default: null,
})
rootDnaId: string;
// @Column({
// nullable: true,
// length: 40,
// comment: "dna ของตาราง orgChild1",
// default: null,
// })
// child1DnaId: string;
@Column({
nullable: true,
length: 40,
comment: "dna ของตาราง orgChild1",
default: null,
})
child1DnaId: string;
// @Column({
// nullable: true,
// length: 40,
// comment: "dna ของตาราง orgChild2",
// default: null,
// })
// child2DnaId: string;
@Column({
nullable: true,
length: 40,
comment: "dna ของตาราง orgChild2",
default: null,
})
child2DnaId: string;
// @Column({
// nullable: true,
// length: 40,
// comment: "dna ของตาราง orgChild3",
// default: null,
// })
// child3DnaId: string;
@Column({
nullable: true,
length: 40,
comment: "dna ของตาราง orgChild3",
default: null,
})
child3DnaId: string;
// @Column({
// nullable: true,
// length: 40,
// comment: "dna ของตาราง orgChild4",
// default: null,
// })
// child4DnaId: string;
@Column({
nullable: true,
length: 40,
comment: "dna ของตาราง orgChild4",
default: null,
})
child4DnaId: string;
}

View file

@ -74,7 +74,7 @@ export class TenureLevelEmployee extends EntityBase {
positionLevel: string;
}
export class CreateTenureLevelOfficer {
export class CreateTenureLevelEmployee {
profileEmployeeId: string;
positionCee: string | null;
days_diff: number | null;

View file

@ -128,4 +128,6 @@ export class viewDirectorActing {
key: string;
@ViewColumn()
positionSign: string;
@ViewColumn()
positionSignChild: string;
}

View file

@ -39,7 +39,7 @@ class CheckAuth {
}
});
}
public async PermissionOrg(req: RequestWithUser, system: string, action: string) {
public async PermissionOrg(req: RequestWithUser, system: string, action: string, isDirector?: boolean) {
if (
req.headers.hasOwnProperty("api_key") &&
req.headers["api_key"] &&
@ -56,7 +56,7 @@ class CheckAuth {
return await new CallAPI()
.GetData(req, `/org/permission/org/${system}/${action}`)
.then(async (x) => {
let privilege = x.privilege;
let privilege = isDirector && isDirector === true ? "CHILD" : x.privilege;
let data: any = {
root: [null],
@ -288,6 +288,9 @@ class CheckAuth {
public async PermissionOrgList(req: RequestWithUser, system: string) {
return await this.PermissionOrg(req, system, "LIST");
}
public async PermissionIsDirectorOrgList(req: RequestWithUser, system: string, isDirector: boolean) {
return await this.PermissionOrg(req, system, "LIST", isDirector);
}
public async PermissionOrgUpdate(req: RequestWithUser, system: string) {
return await this.PermissionOrg(req, system, "UPDATE");
}

View file

@ -280,7 +280,7 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
await AppDataSource.getRepository(PosMaster)
.createQueryBuilder()
.update(PosMaster)
.set({ current_holderId: null })
.set({ current_holderId: null, isSit: false })
.where("id = :id", { id: findProfileInposMaster?.id })
.execute();
@ -293,7 +293,7 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
await AppDataSource.getRepository(PosMaster)
.createQueryBuilder()
.update(PosMaster)
.set({ next_holderId: null })
.set({ next_holderId: null, isSit: false })
.where("id = :id", { id: findProfileInposMasterDraft?.id })
.execute();
@ -326,7 +326,7 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
await AppDataSource.getRepository(EmployeePosMaster)
.createQueryBuilder()
.update(EmployeePosMaster)
.set({ current_holderId: null })
.set({ current_holderId: null, isSit: false })
.where("id = :id", { id: findProfileInEmpPosMaster?.id })
.execute();
@ -395,43 +395,6 @@ export async function checkReturnCommandType(commandId: string) {
return true;
}
export async function checkExceptCommandType(commandId: string) {
const commandRepository = AppDataSource.getRepository(Command);
const commandReciveRepository = AppDataSource.getRepository(CommandRecive);
const _type = await commandRepository.findOne({
where: {
id: commandId,
},
relations: ["commandType"],
});
if (!["C-PM-25", "C-PM-26"].includes(String(_type?.commandType.code))) {
return { status: false, LeaveType: null, leaveRemark: null };
}
const _commandRecive = await commandReciveRepository.findOne({
where: { commandId: commandId },
});
let _leaveType: string = "";
switch (String(_type?.commandType.code)) {
case "C-PM-25": {
_leaveType = "DISCIPLINE_SUSPEND"; //คำสั่งพักจากราชการ
break;
}
case "C-PM-26": {
_leaveType = "DISCIPLINE_TEMP_SUSPEND"; //คำสั่งให้ออกจากราชการไว้ก่อน
break;
}
default: {
_leaveType = "";
}
}
return {
status: true,
LeaveType: _leaveType,
leaveRemark: _commandRecive ? _commandRecive.remarkVertical : null,
};
}
export async function checkCommandType(commandId: string) {
const commandRepository = AppDataSource.getRepository(Command);
const commandReciveRepository = AppDataSource.getRepository(CommandRecive);
@ -451,6 +414,8 @@ export async function checkCommandType(commandId: string) {
"C-PM-23",
"C-PM-19",
"C-PM-20",
"C-PM-25",
"C-PM-26",
"C-PM-43",
].includes(String(_type?.commandType.code))
) {
@ -500,6 +465,16 @@ export async function checkCommandType(commandId: string) {
_retireTypeName = "ลาออกจากราชการ";
break;
}
case "C-PM-25": {
_leaveType = "DISCIPLINE_SUSPEND";
_retireTypeName = "พักจากราชการ";
break;
}
case "C-PM-26": {
_leaveType = "DISCIPLINE_TEMP_SUSPEND";
_retireTypeName = "ให้ออกจากราชการไว้ก่อน";
break;
}
case "C-PM-43": {
_leaveType = "RETIRE_OUT_EMP";
_retireTypeName = "ให้ออกจากราชการ";
@ -752,3 +727,22 @@ export function resolveNodeId(data: any) {
null
);
}
export function logPositionIsSelectedChange(
positionId: string,
oldValue: boolean,
newValue: boolean,
context: {
posMasterId?: string;
userId?: string;
endpoint?: string;
action?: string;
}
) {
if (oldValue !== newValue) {
console.log(`[positionIsSelected-DEBUG] Position ${positionId}: ${oldValue} -> ${newValue}`, {
...context,
timestamp: new Date().toISOString(),
});
}
}

View file

@ -1019,7 +1019,9 @@ export async function resetPassword(username: string) {
if (!users.ok) {
const errorText = await users.text();
console.error(`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`);
console.error(
`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`,
);
return false;
}
@ -1047,7 +1049,9 @@ export async function resetPassword(username: string) {
if (!resetResponse.ok) {
const errorText = await resetResponse.text();
console.error(`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`);
console.error(
`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`,
);
return false;
}
@ -1117,7 +1121,7 @@ export async function updateUserAttributes(
return false;
}
console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`);
// console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`);
return true;
} catch (error) {
console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error);

View file

@ -4,6 +4,7 @@ import { createDecoder, createVerifier } from "fast-jwt";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { handleWebServiceAuth } from "./authWebService";
import { handleInternalAuth } from "./authInternal";
if (!process.env.AUTH_PUBLIC_KEY && !process.env.AUTH_REALM_URL) {
throw new Error("Require keycloak AUTH_PUBLIC_KEY or AUTH_REALM_URL.");
@ -39,6 +40,11 @@ export async function expressAuthentication(
return { preferred_username: "bypassed" };
}
// เพิ่มการจัดการสำหรับ Internal Authentication (.NET service)
if (securityName === "internalAuth") {
return await handleInternalAuth(request);
}
// เพิ่มการจัดการสำหรับ Web Service Authentication
if (securityName === "webServiceAuth") {
return await handleWebServiceAuth(request);

View file

@ -0,0 +1,30 @@
import * as express from "express";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
// Internal Authentication (สำหรับ Internal Service เช่น .NET)
// ตรวจสอบ API Key จาก Environment Variable (API_KEY)
export async function handleInternalAuth(request: express.Request) {
// รองรับ header หลายรูปแบบ
const apiKey =
request.headers["api-key"] || request.headers["api_key"] || request.headers["apikey"];
if (!apiKey || typeof apiKey !== "string") {
throw new HttpError(HttpStatus.UNAUTHORIZED, "API Key is required");
}
// ตรวจสอบ API Key จาก Environment Variable (API_KEY)
if (apiKey !== process.env.API_KEY) {
console.log(`[InternalAuth] Invalid API key attempt: ${apiKey.substring(0, 5)}...`);
throw new HttpError(HttpStatus.UNAUTHORIZED, "Invalid API Key");
}
// console.log(`[InternalAuth] Authentication successful`);
return {
sub: "internal_service",
preferred_username: "internal_service",
name: "Internal Service",
internalKey: true,
};
}

View file

@ -17,7 +17,17 @@ export async function handleWebServiceAuth(request: express.Request) {
// ตรวจสอบ API Key กับฐานข้อมูล
const apiKeyData = await AppDataSource.getRepository(ApiKey).findOne({
select: { id: true, name: true, keyApi: true },
select: {
id: true,
name: true,
keyApi: true,
accessType: true,
dnaRootId: true,
dnaChild1Id: true,
dnaChild2Id: true,
dnaChild3Id: true,
dnaChild4Id: true,
},
where: { keyApi: apiKey },
relations: ["apiNames"],
});
@ -40,6 +50,12 @@ export async function handleWebServiceAuth(request: express.Request) {
name: apiKeyData.name,
type: "web-service",
accessApi: apiKeyData.apiNames.map((x) => x.id) ?? [],
accessType: apiKeyData.accessType,
dnaRootId: apiKeyData.dnaRootId,
dnaChild1Id: apiKeyData.dnaChild1Id,
dnaChild2Id: apiKeyData.dnaChild2Id,
dnaChild3Id: apiKeyData.dnaChild3Id,
dnaChild4Id: apiKeyData.dnaChild4Id,
};
}

View file

@ -25,5 +25,11 @@ export type RequestWithUserWebService = Request & {
id: string;
name: string;
accessApi: string[];
accessType?: string;
dnaRootId?: string | null;
dnaChild1Id?: string | null;
dnaChild2Id?: string | null;
dnaChild3Id?: string | null;
dnaChild4Id?: string | null;
};
};

View file

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdatePosMasterEmpHisAddDna1779244154610 implements MigrationInterface {
name = 'UpdatePosMasterEmpHisAddDna1779244154610'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` ADD \`profileEmployeeId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง profileEmployee'`);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` ADD \`rootDnaId\` varchar(40) NULL COMMENT 'dna ของตาราง orgRoot'`);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` ADD \`child1DnaId\` varchar(40) NULL COMMENT 'dna ของตาราง orgChild1'`);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` ADD \`child2DnaId\` varchar(40) NULL COMMENT 'dna ของตาราง orgChild2'`);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` ADD \`child3DnaId\` varchar(40) NULL COMMENT 'dna ของตาราง orgChild3'`);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` ADD \`child4DnaId\` varchar(40) NULL COMMENT 'dna ของตาราง orgChild4'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` DROP COLUMN \`child4DnaId\``);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` DROP COLUMN \`child3DnaId\``);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` DROP COLUMN \`child2DnaId\``);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` DROP COLUMN \`child1DnaId\``);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` DROP COLUMN \`rootDnaId\``);
await queryRunner.query(`ALTER TABLE \`posMasterEmployeeHistory\` DROP COLUMN \`profileEmployeeId\``);
}
}

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdateCommandAddShortName1779776860350 implements MigrationInterface {
name = 'UpdateCommandAddShortName1779776860350'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`command\` ADD \`shortName\` varchar(16) NULL COMMENT 'ชื่อย่อหน่วยงานที่ออกคำสั่ง'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`command\` DROP COLUMN \`shortName\``);
}
}

View file

@ -0,0 +1,27 @@
import "dotenv/config";
import "reflect-metadata";
import { AppDataSource } from "../database/data-source";
import { clearOldOrgRevisionData } from "../services/ClearOldOrgRevisionService";
// "clear:old-org-revision": "ts-node src/scripts/ClearOldOrgRevision.ts",
const defaultOrgRevisionId = "24dacf63-d289-496c-8102-8b25079dbaf2";
async function main(): Promise<void> {
const orgRevisionId = process.argv[2] || defaultOrgRevisionId;
try {
await AppDataSource.initialize();
const result = await clearOldOrgRevisionData(orgRevisionId);
console.info(JSON.stringify(result, null, 2));
} catch (error) {
console.error("[ClearOldOrgRevision] Failed:", error);
process.exitCode = 1;
} finally {
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
}
}
}
void main();

View file

@ -0,0 +1,232 @@
import { EntityManager, EntityTarget, In } from "typeorm";
import { AppDataSource } from "../database/data-source";
import { OrgRevision } from "../entities/OrgRevision";
import { PosMaster } from "../entities/PosMaster";
import { Position } from "../entities/Position";
import { OrgRoot } from "../entities/OrgRoot";
import { OrgChild1 } from "../entities/OrgChild1";
import { OrgChild2 } from "../entities/OrgChild2";
import { OrgChild3 } from "../entities/OrgChild3";
import { OrgChild4 } from "../entities/OrgChild4";
import { PosMasterAct } from "../entities/PosMasterAct";
import { PosMasterAssign } from "../entities/PosMasterAssign";
import { PermissionOrg } from "../entities/PermissionOrg";
import { PermissionProfile } from "../entities/PermissionProfile";
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster";
import { EmployeePosition } from "../entities/EmployeePosition";
import { orgStructureCache } from "../utils/OrgStructureCache";
export interface ClearOldOrgRevisionSummary {
orgRevisionId: string;
orgRevisionName: string;
deleted: {
positions: number;
employeePositionsByPosMaster: number;
employeePositionsByTempPosMaster: number;
posMasterActsByParent: number;
posMasterActsByChild: number;
posMasterAssigns: number;
posMasters: number;
employeePosMasters: number;
employeeTempPosMasters: number;
permissionOrgs: number;
permissionProfiles: number;
orgChild4s: number;
orgChild3s: number;
orgChild2s: number;
orgChild1s: number;
orgRoots: number;
orgRevisions: number;
};
}
interface OrgRevisionSnapshot {
id: string;
orgRevisionName: string;
orgRevisionIsCurrent: boolean;
orgRevisionIsDraft: boolean;
}
export async function clearOldOrgRevisionData(
orgRevisionId: string,
): Promise<ClearOldOrgRevisionSummary> {
const result = await AppDataSource.transaction(async (manager) => {
const orgRevision = await manager.findOne(OrgRevision, {
where: { id: orgRevisionId },
select: ["id", "orgRevisionName", "orgRevisionIsCurrent", "orgRevisionIsDraft"],
});
if (!orgRevision) {
throw new Error(`ไม่พบ orgRevision ที่ต้องการล้างข้อมูล: ${orgRevisionId}`);
}
validateOrgRevisionForDeletion(orgRevision);
const [posMasters, orgRoots, employeePosMasters, employeeTempPosMasters] = await Promise.all([
manager.find(PosMaster, {
where: { orgRevisionId },
select: ["id"],
}),
manager.find(OrgRoot, {
where: { orgRevisionId },
select: ["id"],
}),
manager.find(EmployeePosMaster, {
where: { orgRevisionId },
select: ["id"],
}),
manager.find(EmployeeTempPosMaster, {
where: { orgRevisionId },
select: ["id"],
}),
]);
const posMasterIds = posMasters.map((item) => item.id);
const orgRootIds = orgRoots.map((item) => item.id);
const employeePosMasterIds = employeePosMasters.map((item) => item.id);
const employeeTempPosMasterIds = employeeTempPosMasters.map((item) => item.id);
const [
positionsCount,
employeePositionsByPosMasterCount,
employeePositionsByTempPosMasterCount,
posMasterActsByParentCount,
posMasterActsByChildCount,
posMasterAssignsCount,
permissionOrgsCount,
permissionProfilesCount,
orgChild4sCount,
orgChild3sCount,
orgChild2sCount,
orgChild1sCount,
] = await Promise.all([
countByIds(manager, Position, "posMasterId", posMasterIds),
countByIds(manager, EmployeePosition, "posMasterId", employeePosMasterIds),
countByIds(manager, EmployeePosition, "posMasterTempId", employeeTempPosMasterIds),
countByIds(manager, PosMasterAct, "posMasterId", posMasterIds),
countByIds(manager, PosMasterAct, "posMasterChildId", posMasterIds),
countByIds(manager, PosMasterAssign, "posMasterId", posMasterIds),
countByIds(manager, PermissionOrg, "orgRootId", orgRootIds),
countByIds(manager, PermissionProfile, "orgRootId", orgRootIds),
manager.count(OrgChild4, { where: { orgRevisionId } }),
manager.count(OrgChild3, { where: { orgRevisionId } }),
manager.count(OrgChild2, { where: { orgRevisionId } }),
manager.count(OrgChild1, { where: { orgRevisionId } }),
]);
if (positionsCount > 0) {
await manager.delete(Position, { posMasterId: In(posMasterIds) });
}
if (employeePositionsByPosMasterCount > 0) {
await manager.delete(EmployeePosition, { posMasterId: In(employeePosMasterIds) });
}
if (employeePositionsByTempPosMasterCount > 0) {
await manager.delete(EmployeePosition, { posMasterTempId: In(employeeTempPosMasterIds) });
}
if (posMasterActsByParentCount > 0) {
await manager.delete(PosMasterAct, { posMasterId: In(posMasterIds) });
}
if (posMasterActsByChildCount > 0) {
await manager.delete(PosMasterAct, { posMasterChildId: In(posMasterIds) });
}
if (posMasterAssignsCount > 0) {
await manager.delete(PosMasterAssign, { posMasterId: In(posMasterIds) });
}
const posMastersCount = posMasterIds.length;
const employeePosMastersCount = employeePosMasterIds.length;
const employeeTempPosMastersCount = employeeTempPosMasterIds.length;
if (posMastersCount > 0) {
await manager.delete(PosMaster, { orgRevisionId });
}
if (employeePosMastersCount > 0) {
await manager.delete(EmployeePosMaster, { orgRevisionId });
}
if (employeeTempPosMastersCount > 0) {
await manager.delete(EmployeeTempPosMaster, { orgRevisionId });
}
if (permissionOrgsCount > 0) {
await manager.delete(PermissionOrg, { orgRootId: In(orgRootIds) });
}
if (permissionProfilesCount > 0) {
await manager.delete(PermissionProfile, { orgRootId: In(orgRootIds) });
}
if (orgChild4sCount > 0) {
await manager.delete(OrgChild4, { orgRevisionId });
}
if (orgChild3sCount > 0) {
await manager.delete(OrgChild3, { orgRevisionId });
}
if (orgChild2sCount > 0) {
await manager.delete(OrgChild2, { orgRevisionId });
}
if (orgChild1sCount > 0) {
await manager.delete(OrgChild1, { orgRevisionId });
}
const orgRootsCount = orgRootIds.length;
if (orgRootsCount > 0) {
await manager.delete(OrgRoot, { orgRevisionId });
}
await manager.delete(OrgRevision, { id: orgRevisionId });
return {
orgRevisionId: orgRevision.id,
orgRevisionName: orgRevision.orgRevisionName,
deleted: {
positions: positionsCount,
employeePositionsByPosMaster: employeePositionsByPosMasterCount,
employeePositionsByTempPosMaster: employeePositionsByTempPosMasterCount,
posMasterActsByParent: posMasterActsByParentCount,
posMasterActsByChild: posMasterActsByChildCount,
posMasterAssigns: posMasterAssignsCount,
posMasters: posMastersCount,
employeePosMasters: employeePosMastersCount,
employeeTempPosMasters: employeeTempPosMastersCount,
permissionOrgs: permissionOrgsCount,
permissionProfiles: permissionProfilesCount,
orgChild4s: orgChild4sCount,
orgChild3s: orgChild3sCount,
orgChild2s: orgChild2sCount,
orgChild1s: orgChild1sCount,
orgRoots: orgRootsCount,
orgRevisions: 1,
},
};
});
orgStructureCache.invalidate(orgRevisionId);
return result;
}
function validateOrgRevisionForDeletion(orgRevision: OrgRevisionSnapshot): void {
if (orgRevision.orgRevisionIsCurrent) {
throw new Error(`ไม่สามารถลบ orgRevision ปัจจุบันได้: ${orgRevision.id}`);
}
if (orgRevision.orgRevisionIsDraft) {
throw new Error(`ไม่สามารถลบ orgRevision แบบร่างได้ด้วยสคริปต์นี้: ${orgRevision.id}`);
}
}
async function countByIds<Entity extends object>(
manager: EntityManager,
entity: EntityTarget<Entity>,
field: keyof Entity,
ids: string[],
): Promise<number> {
if (ids.length === 0) {
return 0;
}
const alias = "entity";
return manager
.createQueryBuilder(entity, alias)
.where(`${alias}.${String(field)} IN (:...ids)`, { ids })
.getCount();
}

View file

@ -530,18 +530,20 @@ export class KeycloakAttributeService {
// 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`);
console.log(
`[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`,
);
}
// Select repository based on profile type
const repo =
profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo;
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.isDelete = :isDelete", { isDelete: false })
.andWhere("p.lastUpdatedAt BETWEEN :start AND :end", {
start: startDate,
end: endDate,
@ -579,8 +581,7 @@ export class KeycloakAttributeService {
try {
// Check if empType is empty in Keycloak
const { isEmpty, currentEmpType } =
await this.checkEmpTypeEmpty(keycloakUserId);
const { isEmpty, currentEmpType } = await this.checkEmpTypeEmpty(keycloakUserId);
result.profilesChecked++;
@ -607,8 +608,7 @@ export class KeycloakAttributeService {
// Sync the profile
const success = await withRetry(
async () =>
this.syncOnOrganizationChange(profile.id, profileType),
async () => this.syncOnOrganizationChange(profile.id, profileType),
3, // maxRetries
1000, // baseDelay
);
@ -768,7 +768,13 @@ export class KeycloakAttributeService {
maxRetries?: number; // Retry attempts for failed operations
rateLimit?: number; // Requests per second
clearProgress?: boolean; // Start fresh, ignore existing progress
}): Promise<{ total: number; success: number; failed: number; details: any[]; resumed?: boolean }> {
}): Promise<{
total: number;
success: number;
failed: number;
details: any[];
resumed?: boolean;
}> {
const limit = options?.limit;
const concurrency = options?.concurrency ?? 5;
const resume = options?.resume ?? false;
@ -922,7 +928,10 @@ export class KeycloakAttributeService {
// Save progress after each batch
SyncProgressManager.save(updatedState);
// Log progress every 50 items
if (updatedState.lastSyncedIndex % 50 === 0 || updatedState.lastSyncedIndex === updatedState.totalProfiles) {
if (
updatedState.lastSyncedIndex % 50 === 0 ||
updatedState.lastSyncedIndex === updatedState.totalProfiles
) {
SyncProgressManager.logProgress(updatedState);
}
},

View file

@ -188,6 +188,7 @@ export async function CreatePosMasterHistoryOfficer(
return true;
} catch (err) {
if (manager) {
console.error("CreatePosMasterHistoryOfficer error (external transaction):", err);
throw err;
}
console.error("CreatePosMasterHistoryOfficer transaction error:", err);
@ -230,6 +231,7 @@ export async function CreatePosMasterHistoryEmployee(
: null;
h.ancestorDNA = pm.ancestorDNA;
if (!type || type != "DELETE") {
h.profileEmployeeId = pm.current_holder?.id || _null;
h.prefix = pm.current_holder?.prefix || _null;
h.firstName = pm.current_holder?.firstName || _null;
h.lastName = pm.current_holder?.lastName || _null;
@ -237,6 +239,11 @@ export async function CreatePosMasterHistoryEmployee(
h.posType = selectedPosition?.posType?.posTypeName ?? _null;
h.posLevel = selectedPosition?.posLevel?.posLevelName ?? _null;
}
h.rootDnaId = pm.orgRoot?.ancestorDNA || _null;
h.child1DnaId = pm.orgChild1?.ancestorDNA || _null;
h.child2DnaId = pm.orgChild2?.ancestorDNA || _null;
h.child3DnaId = pm.orgChild3?.ancestorDNA || _null;
h.child4DnaId = pm.orgChild4?.ancestorDNA || _null;
h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null;
h.posMasterNo = pm.posMasterNo ?? _null;
h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null;
@ -494,3 +501,61 @@ export async function BatchSavePosMasterHistoryOfficer(
return false;
}
}
/**
* profile
* -
* profile
*
* @param profileId ID profile
* @param request RequestWithUser
* @param type "OFFICER" | "EMPLOYEE" (default: "OFFICER")
*/
export async function updateHolderProfileHistory(
profileId: string,
request: RequestWithUser,
type: "OFFICER" | "EMPLOYEE" = "OFFICER",
): Promise<void> {
try {
if (type === "OFFICER") {
const posMasterRepo = AppDataSource.getRepository(PosMaster);
const posMaster = await posMasterRepo.findOne({
where: {
current_holderId: profileId,
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
}
},
relations: {
orgRevision : true
}
});
if (posMaster) {
await CreatePosMasterHistoryOfficer(posMaster.id, request);
}
} else if (type === "EMPLOYEE") {
const empPosMasterRepo = AppDataSource.getRepository(EmployeePosMaster);
const employeePosMaster = await empPosMasterRepo.findOne({
where: {
current_holderId: profileId,
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
}
},
relations: {
orgRevision : true
}
});
if (employeePosMaster) {
await CreatePosMasterHistoryEmployee(employeePosMaster.id, request);
}
}
} catch (error) {
console.error("updateHolderProfileHistory error:", error);
throw error;
}
}

View file

@ -1,12 +1,13 @@
import { AppDataSource } from "../database/data-source";
import { Profile } from "./../entities/Profile";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import { ProfileSalary } from "./../entities/ProfileSalary";
import { OrgRoot } from "../entities/OrgRoot";
import { OrgChild1 } from "../entities/OrgChild1";
import { OrgChild2 } from "../entities/OrgChild2";
import { OrgChild3 } from "../entities/OrgChild3";
import { OrgChild4 } from "../entities/OrgChild4";
import { Brackets, Repository } from "typeorm";
import { Brackets, In, Repository } from "typeorm";
import Extension from "../interfaces/extension";
import { RequestWithUser } from "../middlewares/user";
@ -62,6 +63,7 @@ interface OrgParentName {
export class ProfileLeaveService {
private profileEmployeeRepo: Repository<ProfileEmployee>;
private profileRepo: Repository<Profile>;
private profileSalaryRepo: Repository<ProfileSalary>;
private orgRootRepository: Repository<OrgRoot>;
private child1Repository: Repository<OrgChild1>;
private child2Repository: Repository<OrgChild2>;
@ -72,6 +74,7 @@ export class ProfileLeaveService {
constructor() {
this.profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
this.profileRepo = AppDataSource.getRepository(Profile);
this.profileSalaryRepo = AppDataSource.getRepository(ProfileSalary);
this.orgRootRepository = AppDataSource.getRepository(OrgRoot);
this.child1Repository = AppDataSource.getRepository(OrgChild1);
this.child2Repository = AppDataSource.getRepository(OrgChild2);
@ -207,10 +210,9 @@ export class ProfileLeaveService {
let params: NodeParams = {};
const orgLists = await this.findOrgNodeParentAll(node, nodeId);
console.log("Org Hierarchy for Node Condition:", orgLists);
await Promise.all(
this.nodeConfigs.map(async (config, index) => {
if (index <= node) {
for (let index = 0; index <= node; index++) {
const config = this.nodeConfigs[index];
const orgName = orgLists[config.nameField as keyof OrgParentName] || null;
if (orgName) {
nodeCondition += index > 0 ? ` AND ${config.condition}` : config.condition;
@ -218,8 +220,6 @@ export class ProfileLeaveService {
params[config.paramKey] = orgName;
}
}
}),
);
return {
condition: nodeCondition,
@ -234,53 +234,31 @@ export class ProfileLeaveService {
child3: string | null;
child4: string | null;
}): Promise<OrgParentName> {
const orgNames: OrgParentName = {
orgRootName: null,
orgChild1Name: null,
orgChild2Name: null,
orgChild3Name: null,
orgChild4Name: null,
const [rootName, child1, child2, child3, child4] = await Promise.all([
orgIds.root
? this.orgRootRepository.findOne({ where: { id: orgIds.root }, select: ["orgRootName"] })
: Promise.resolve(null),
orgIds.child1
? this.child1Repository.findOne({ where: { id: orgIds.child1 }, select: ["orgChild1Name"] })
: Promise.resolve(null),
orgIds.child2
? this.child2Repository.findOne({ where: { id: orgIds.child2 }, select: ["orgChild2Name"] })
: Promise.resolve(null),
orgIds.child3
? this.child3Repository.findOne({ where: { id: orgIds.child3 }, select: ["orgChild3Name"] })
: Promise.resolve(null),
orgIds.child4
? this.child4Repository.findOne({ where: { id: orgIds.child4 }, select: ["orgChild4Name"] })
: Promise.resolve(null),
]);
return {
orgRootName: rootName?.orgRootName ?? null,
orgChild1Name: child1?.orgChild1Name ?? null,
orgChild2Name: child2?.orgChild2Name ?? null,
orgChild3Name: child3?.orgChild3Name ?? null,
orgChild4Name: child4?.orgChild4Name ?? null,
};
if (orgIds.root) {
const rootName = await this.orgRootRepository.findOne({
where: { id: orgIds.root },
select: ["orgRootName"],
});
orgNames.orgRootName = rootName ? rootName.orgRootName : null;
}
if (orgIds.child1) {
const child1 = await this.child1Repository.findOne({
where: { id: orgIds.child1 },
select: ["orgChild1Name"],
});
orgNames.orgChild1Name = child1 ? child1.orgChild1Name : null;
}
if (orgIds.child2) {
const child2 = await this.child2Repository.findOne({
where: { id: orgIds.child2 },
select: ["orgChild2Name"],
});
orgNames.orgChild2Name = child2 ? child2.orgChild2Name : null;
}
if (orgIds.child3) {
const child3 = await this.child3Repository.findOne({
where: { id: orgIds.child3 },
select: ["orgChild3Name"],
});
orgNames.orgChild3Name = child3 ? child3.orgChild3Name : null;
}
if (orgIds.child4) {
const child4 = await this.child4Repository.findOne({
where: { id: orgIds.child4 },
select: ["orgChild4Name"],
});
orgNames.orgChild4Name = child4 ? child4.orgChild4Name : null;
}
return orgNames;
}
/** สร้างเงื่อนไขการค้นหาตาม node และ nodeId และเช็คกับ permission */
@ -317,16 +295,15 @@ export class ProfileLeaveService {
return { condition: "1=0", params: {} }; // no access
}
await Promise.all(
this.nodeConfigs.map(async (config, index) => {
for (let index = 0; index < this.nodeConfigs.length; index++) {
const config = this.nodeConfigs[index];
const orgName = orgLists[config.nameField as keyof OrgParentName] || null;
if (orgName) {
nodeCondition += index > 0 ? ` AND ${config.condition}` : config.condition;
nodeCondition += isAll === false && config.isAllTrue ? ` AND ${config.isAllTrue}` : "";
params[config.paramKey] = orgName;
}
}),
);
}
return {
condition: nodeCondition,
@ -478,33 +455,14 @@ export class ProfileLeaveService {
_data,
} = filter;
const t0 = Date.now();
const searchQuery = this.buildSearchQuery(searchField, "profileEmployee");
// สร้าง main query - เปลี่ยนจาก leftJoinAndSelect เป็น leftJoin สำหรับ profileSalary
const queryBuilder = this.profileEmployeeRepo
.createQueryBuilder("profileEmployee")
.leftJoinAndSelect("profileEmployee.posLevel", "posLevel")
.leftJoinAndSelect("profileEmployee.posType", "posType")
.leftJoinAndSelect("profileEmployee.profileEmployeeEmployment", "profileEmployeeEmployment")
.leftJoin(
"profileEmployee.profileSalary",
"profileSalary",
"profileSalary.order = (SELECT MAX(ps.order) FROM profileSalary ps WHERE ps.profileEmployeeId = profileEmployee.id and ps.positionName != 'เกษียณอายุราชการ')",
)
.addSelect([
"profileSalary.id",
"profileSalary.order",
"profileSalary.posNo",
"profileSalary.posNoAbb",
"profileSalary.orgRoot",
"profileSalary.orgChild1",
"profileSalary.orgChild2",
"profileSalary.orgChild3",
"profileSalary.orgChild4",
])
.where(
new Brackets((qb) => {
qb.where("profileEmployee.isLeave = :isLeave", { isLeave: true }).orWhere(
// สร้าง base WHERE conditions แชร์ระหว่าง count/id/data query
const baseWhere = (qb: any) => {
qb.where(
new Brackets((qb2) => {
qb2.where("profileEmployee.isLeave = :isLeave", { isLeave: true }).orWhere(
"profileEmployee.isRetirement = :isRetirement",
{ isRetirement: true },
);
@ -512,63 +470,131 @@ export class ProfileLeaveService {
)
.andWhere("profileEmployee.employeeClass LIKE :type", { type: "PERM" })
.andWhere(
new Brackets((qb) => {
qb.orWhere(searchKeyword && searchKeyword != "" ? searchQuery : "1=1", {
new Brackets((qb2) => {
qb2.orWhere(searchKeyword && searchKeyword != "" ? searchQuery : "1=1", {
keyword: `%${searchKeyword}%`,
});
}),
);
// เพิ่มเงื่อนไขการค้นหา
if (posType) {
queryBuilder.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` });
qb.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` });
}
if (posLevel) {
queryBuilder.andWhere(
qb.andWhere(
"CONCAT(posType.posTypeShortName, ' ', posLevel.posLevelName) LIKE :keyword2",
{ keyword2: `${posLevel}` },
);
}
if (isProbation) {
queryBuilder.andWhere(`profileEmployee.isProbation = ${isProbation}`);
if (isProbation !== undefined && isProbation !== null) {
qb.andWhere("profileEmployee.isProbation = :isProbation", { isProbation });
}
if (retireType) {
queryBuilder.andWhere("profileEmployee.leaveType = :retireType", { retireType });
qb.andWhere("profileEmployee.leaveType = :retireType", { retireType });
}
};
if (node !== null && node !== undefined && nodeId) {
const [nodeCondition, permissionCondition] = await Promise.all([
this.buildNodeCondition(node, nodeId, isAll),
this.buildPermissionCondition(_data, isAll),
]);
// console.log("Permission Condition:", permissionCondition);
// console.log("Node Condition:", nodeCondition);
queryBuilder.andWhere(nodeCondition.condition, nodeCondition.params);
// Compute permission/node conditions เพียงครั้งเดียว
const conditions: { condition: string; params: Record<string, any> }[] = [];
if (_data.privilege !== "OWNER" && _data.privilege !== "PARENT") {
queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params);
conditions.push(await this.buildPermissionCondition(_data, isAll));
}
if (node !== null && node !== undefined && nodeId) {
conditions.push(await this.buildNodeCondition(node, nodeId, isAll));
}
const applyConditions = (qb: any) => {
for (const cond of conditions) {
qb.andWhere(cond.condition, cond.params);
}
};
// เพิ่ม sorting และ pagination
queryBuilder
.orderBy(sortBy, sort)
.skip((page - 1) * pageSize)
.take(pageSize);
// console.log(`[ProfileLeaveService] getLeaveEmployees conditions took ${Date.now() - t0}ms`);
const [records, total] = await queryBuilder.getManyAndCount();
// print query for debug
// console.log("SQL Query:", queryBuilder.getSql());
const data = await Promise.all(
records.map((record) => Promise.resolve(this.transformEmployeeData(record))),
// สร้าง salary EXISTS filter (ใช้ซ้ำทั้ง step1, step2)
const applySalaryFilter = (qb: any) => {
if (conditions.length > 0) {
let existsCond = "profileSalary.positionName != :notRetire";
const existsParams: Record<string, any> = { notRetire: "เกษียณอายุราชการ" };
for (const cond of conditions) {
existsCond += ` AND ${cond.condition}`;
Object.assign(existsParams, cond.params);
}
qb.andWhere(
`EXISTS (SELECT 1 FROM profileSalary WHERE profileEmployeeId = profileEmployee.id AND ${existsCond} AND profileSalary.\`order\` = (SELECT MAX(ps.\`order\`) FROM profileSalary ps WHERE ps.profileEmployeeId = profileEmployee.id AND ps.positionName != :notRetire2))`,
{ ...existsParams, notRetire2: "เกษียณอายุราชการ" }
);
}
};
// Step 1: Count query
const countQb = this.profileEmployeeRepo
.createQueryBuilder("profileEmployee")
.leftJoinAndSelect("profileEmployee.posLevel", "posLevel")
.leftJoinAndSelect("profileEmployee.posType", "posType");
baseWhere(countQb);
applySalaryFilter(countQb);
const total = await countQb.getCount();
// console.log(`[ProfileLeaveService] getLeaveEmployees count took ${Date.now() - t0}ms, total=${total}`);
// Step 2: ดึงเฉพาะ profileEmployee IDs ที่ผ่านเงื่อนไข
const idQb = this.profileEmployeeRepo
.createQueryBuilder("profileEmployee")
.select(["profileEmployee.id"])
.leftJoin("profileEmployee.posLevel", "posLevel")
.leftJoin("profileEmployee.posType", "posType");
baseWhere(idQb);
applySalaryFilter(idQb);
idQb.orderBy(sortBy, sort).skip((page - 1) * pageSize).take(pageSize);
const rawIds = await idQb.getRawMany();
const employeeIds = rawIds.map((r) => r.profileEmployee_id);
// console.log(`[ProfileLeaveService] getLeaveEmployees ids took ${Date.now() - t0}ms, ids=${employeeIds.length}`);
if (employeeIds.length === 0) {
return { data: [], total };
}
// Step 3: Load full data โดยไม่ JOIN salary
const records = await this.profileEmployeeRepo.find({
where: { id: In(employeeIds) },
relations: ["posLevel", "posType", "profileEmployeeEmployment"],
order: { [sortBy.split(".")[1]]: sort } as any,
});
// Step 4: Load salary เฉพาะ row ที่มี order สูงสุดต่อ profileEmployeeId (INNER JOIN + GROUP BY)
const salaries = await this.profileSalaryRepo
.createQueryBuilder("ps")
.innerJoin(
(subQuery) =>
subQuery
.select("ps2.profileEmployeeId", "pid")
.addSelect("MAX(ps2.order)", "maxOrd")
.from(ProfileSalary, "ps2")
.where("ps2.profileEmployeeId IN (:...employeeIds)", { employeeIds })
.andWhere("ps2.positionName != :notRetire", { notRetire: "เกษียณอายุราชการ" })
.groupBy("ps2.profileEmployeeId"),
"latest",
"latest.pid = ps.profileEmployeeId AND ps.order = latest.maxOrd"
)
.getMany();
// สร้าง map: profileEmployeeId → salary ที่มี order สูงสุด
const salaryMap = new Map<string, ProfileSalary>();
for (const s of salaries) {
salaryMap.set(s.profileEmployeeId, s);
}
// แปลงข้อมูลพร้อม salary
const data = records.map((record) => {
const salary = salaryMap.get(record.id);
if (salary) {
(record as any).profileSalary = [salary];
}
return this.transformEmployeeData(record);
});
// console.log(`[ProfileLeaveService] getLeaveEmployees total took ${Date.now() - t0}ms, total=${total}`);
return { data, total };
}
@ -649,94 +675,143 @@ export class ProfileLeaveService {
_data,
} = filter;
const t0 = Date.now();
const searchQuery = this.buildSearchQuery(searchField);
// สร้าง main query - เปลี่ยนจาก leftJoinAndSelect เป็น leftJoin สำหรับ profileSalary
const queryBuilder = this.profileRepo
.createQueryBuilder("profile")
.leftJoinAndSelect("profile.posLevel", "posLevel")
.leftJoinAndSelect("profile.posType", "posType")
.leftJoin(
"profile.profileSalary",
"profileSalary",
"profileSalary.order = (SELECT MAX(ps.order) FROM profileSalary ps WHERE ps.profileId = profile.id and ps.positionName != 'เกษียณอายุราชการ')",
)
.addSelect([
"profileSalary.id",
"profileSalary.order",
"profileSalary.posNo",
"profileSalary.posNoAbb",
"profileSalary.orgRoot",
"profileSalary.orgChild1",
"profileSalary.orgChild2",
"profileSalary.orgChild3",
"profileSalary.orgChild4",
"profileSalary.positionExecutive",
])
.where(
new Brackets((qb) => {
qb.where("profile.isLeave = :isLeave", { isLeave: true }).orWhere(
// สร้าง base WHERE conditions แชร์ระหว่าง count/id/data query
const baseWhere = (qb: any) => {
qb.where(
new Brackets((qb2) => {
qb2.where("profile.isLeave = :isLeave", { isLeave: true }).orWhere(
"profile.isRetirement = :isRetirement",
{ isRetirement: true },
);
}),
)
.andWhere(
new Brackets((qb) => {
qb.orWhere(searchKeyword && searchKeyword != "" ? searchQuery : "1=1", {
).andWhere(
new Brackets((qb2) => {
qb2.orWhere(searchKeyword && searchKeyword != "" ? searchQuery : "1=1", {
keyword: `%${searchKeyword}%`,
});
}),
);
if (posType) {
queryBuilder.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` });
qb.andWhere("posType.posTypeName LIKE :keyword1", { keyword1: `${posType}` });
}
if (posLevel) {
queryBuilder.andWhere("posLevel.posLevelName LIKE :keyword2", { keyword2: `${posLevel}` });
qb.andWhere("posLevel.posLevelName LIKE :keyword2", { keyword2: `${posLevel}` });
}
if (isProbation) {
queryBuilder.andWhere(`profile.isProbation = ${isProbation}`);
if (isProbation !== undefined && isProbation !== null) {
qb.andWhere("profile.isProbation = :isProbation", { isProbation });
}
if (retireType) {
queryBuilder.andWhere("profile.leaveType = :retireType", { retireType });
qb.andWhere("profile.leaveType = :retireType", { retireType });
}
};
// เพิ่ม permission และ node conditions
if (node !== null && node !== undefined && nodeId) {
// สร้าง query conditions แบบ parallel
const [nodeCondition, permissionCondition] = await Promise.all([
this.buildNodeCondition(node, nodeId, isAll),
this.buildPermissionCondition(_data, isAll),
]);
console.log("Permission Condition:", permissionCondition);
console.log("Node Condition:", nodeCondition);
queryBuilder.andWhere(nodeCondition.condition, nodeCondition.params);
// Compute permission/node conditions เพียงครั้งเดียว
const conditions: { condition: string; params: Record<string, any> }[] = [];
if (_data.privilege !== "OWNER" && _data.privilege !== "PARENT") {
queryBuilder.andWhere(permissionCondition.condition, permissionCondition.params);
conditions.push(await this.buildPermissionCondition(_data, isAll));
}
if (node !== null && node !== undefined && nodeId) {
conditions.push(await this.buildNodeCondition(node, nodeId, isAll));
}
const applyConditions = (qb: any) => {
for (const cond of conditions) {
qb.andWhere(cond.condition, cond.params);
}
};
// เพิ่ม sorting และ pagination
queryBuilder
.orderBy(sortBy, sort)
.skip((page - 1) * pageSize)
.take(pageSize);
// console.log(`[ProfileLeaveService] getLeaveOfficer conditions took ${Date.now() - t0}ms`);
const [records, total] = await queryBuilder.getManyAndCount();
// print query for debug
// console.log("SQL Query:", queryBuilder.getSql());
const data = await Promise.all(
records.map((record) => Promise.resolve(this.transformOfficerData(record))),
// สร้าง salary EXISTS filter (ใช้ซ้ำทั้ง step1, step2)
const applySalaryFilter = (qb: any) => {
if (conditions.length > 0) {
let existsCond = "profileSalary.positionName != :notRetire";
const existsParams: Record<string, any> = { notRetire: "เกษียณอายุราชการ" };
for (const cond of conditions) {
existsCond += ` AND ${cond.condition}`;
Object.assign(existsParams, cond.params);
}
qb.andWhere(
`EXISTS (SELECT 1 FROM profileSalary WHERE profileId = profile.id AND ${existsCond} AND profileSalary.\`order\` = (SELECT MAX(ps.\`order\`) FROM profileSalary ps WHERE ps.profileId = profile.id AND ps.positionName != :notRetire2))`,
{ ...existsParams, notRetire2: "เกษียณอายุราชการ" }
);
}
};
// Step 1: Count query
const countQb = this.profileRepo
.createQueryBuilder("profile")
.leftJoinAndSelect("profile.posLevel", "posLevel")
.leftJoinAndSelect("profile.posType", "posType");
baseWhere(countQb);
applySalaryFilter(countQb);
const total = await countQb.getCount();
// console.log(`[ProfileLeaveService] getLeaveOfficer count took ${Date.now() - t0}ms, total=${total}`);
// Step 2: ดึงเฉพาะ profile IDs ที่ผ่านเงื่อนไข
const idQb = this.profileRepo
.createQueryBuilder("profile")
.select(["profile.id"])
.leftJoin("profile.posLevel", "posLevel")
.leftJoin("profile.posType", "posType");
baseWhere(idQb);
applySalaryFilter(idQb);
idQb.orderBy(sortBy, sort).skip((page - 1) * pageSize).take(pageSize);
const rawIds = await idQb.getRawMany();
const profileIds = rawIds.map((r) => r.profile_id);
// console.log(`[ProfileLeaveService] getLeaveOfficer ids took ${Date.now() - t0}ms, ids=${profileIds.length}`);
if (profileIds.length === 0) {
return { data: [], total };
}
// Step 3: Load full data โดยไม่ JOIN salary
const records = await this.profileRepo.find({
where: { id: In(profileIds) },
relations: ["posLevel", "posType"],
order: { [sortBy.split(".")[1]]: sort } as any,
});
// console.log(`[ProfileLeaveService] getLeaveOfficer step3 (load profiles) took ${Date.now() - t0}ms`);
// Step 4: Load salary เฉพาะ row ที่มี order สูงสุดต่อ profileId (INNER JOIN + GROUP BY)
const salaries = await this.profileSalaryRepo
.createQueryBuilder("ps")
.innerJoin(
(subQuery) =>
subQuery
.select("ps2.profileId", "pid")
.addSelect("MAX(ps2.order)", "maxOrd")
.from(ProfileSalary, "ps2")
.where("ps2.profileId IN (:...profileIds)", { profileIds })
.andWhere("ps2.positionName != :notRetire", { notRetire: "เกษียณอายุราชการ" })
.groupBy("ps2.profileId"),
"latest",
"latest.pid = ps.profileId AND ps.order = latest.maxOrd"
)
.getMany();
// console.log(`[ProfileLeaveService] getLeaveOfficer step4 (load salaries) took ${Date.now() - t0}ms, salary rows=${salaries.length}`);
// สร้าง map: profileId → salary ที่มี order สูงสุด
const salaryMap = new Map<string, ProfileSalary>();
for (const s of salaries) {
salaryMap.set(s.profileId, s);
}
// แปลงข้อมูลพร้อม salary
const data = records.map((record) => {
const salary = salaryMap.get(record.id);
if (salary) {
(record as any).profileSalary = [salary];
}
return this.transformOfficerData(record);
});
// console.log(`[ProfileLeaveService] getLeaveOfficer total took ${Date.now() - t0}ms, total=${total}`);
return { data, total };
}
}

View file

@ -1,5 +1,6 @@
import { randomUUID } from "crypto";
import amqp from "amqplib";
import { promisify } from "util";
import { AppDataSource } from "../database/data-source";
import { Command } from "../entities/Command";
import { chunkArray, commandTypePath } from "../interfaces/utils";
@ -29,6 +30,10 @@ import { PayloadSendNoti } from "../interfaces/utils";
import { PermissionProfile } from "../entities/PermissionProfile";
import { PosMasterHistory } from "../entities/PosMasterHistory";
const redis = require("redis");
const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = process.env.REDIS_PORT;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
function scheduleReconnect() {
@ -143,7 +148,9 @@ function createConsumer( //----> consumer
console.log("[AMQ] Process Consumer success");
return channel.ack(msg);
}
console.error(`[AMQ] Process Consumer failed on queue ${queue}, acknowledging without retry`);
console.error(
`[AMQ] Process Consumer failed on queue ${queue}, acknowledging without retry`,
);
return channel.ack(msg);
} catch (error) {
console.error(`[AMQ] Consumer processing error on queue ${queue}:`, error);
@ -547,19 +554,19 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
const repoPosmaster = AppDataSource.getRepository(PosMaster);
const posMasterAssignRepository = AppDataSource.getRepository(PosMasterAssign);
const posMasterActRepository = AppDataSource.getRepository(PosMasterAct);
const permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile);
const repoEmployeePosmaster = AppDataSource.getRepository(EmployeePosMaster);
const repoEmployeeTempPosmaster = AppDataSource.getRepository(EmployeeTempPosMaster);
// const permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile);
// const repoEmployeePosmaster = AppDataSource.getRepository(EmployeePosMaster);
// const repoEmployeeTempPosmaster = AppDataSource.getRepository(EmployeeTempPosMaster);
const repoProfile = AppDataSource.getRepository(Profile);
const repoProfileEmployee = AppDataSource.getRepository(ProfileEmployee);
const employeePositionRepository = AppDataSource.getRepository(EmployeePosition);
// const repoProfileEmployee = AppDataSource.getRepository(ProfileEmployee);
// const employeePositionRepository = AppDataSource.getRepository(EmployeePosition);
const repoOrgRevision = AppDataSource.getRepository(OrgRevision);
const orgRootRepository = AppDataSource.getRepository(OrgRoot);
const child1Repository = AppDataSource.getRepository(OrgChild1);
const child2Repository = AppDataSource.getRepository(OrgChild2);
const child3Repository = AppDataSource.getRepository(OrgChild3);
const child4Repository = AppDataSource.getRepository(OrgChild4);
const { data, token, user } = JSON.parse(msg.content.toString());
// const orgRootRepository = AppDataSource.getRepository(OrgRoot);
// const child1Repository = AppDataSource.getRepository(OrgChild1);
// const child2Repository = AppDataSource.getRepository(OrgChild2);
// const child3Repository = AppDataSource.getRepository(OrgChild3);
// const child4Repository = AppDataSource.getRepository(OrgChild4);
const { data, user } = JSON.parse(msg.content.toString());
const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data;
console.log(`[AMQ] Received message - revisionId: ${id}, status: ${status}`);
@ -994,7 +1001,7 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
if (posMasterUpdates.length > 0) {
const chunks = chunkArray(posMasterUpdates, 500);
const posMasterTableName = repoPosmaster.metadata.tableName;
for (const chunk of chunks as typeof posMasterUpdates[]) {
for (const chunk of chunks as (typeof posMasterUpdates)[]) {
const caseClauses = chunk.map(() => "WHEN ? THEN ?").join(" ");
const wherePlaceholders = chunk.map(() => "?").join(", ");
const params = chunk.flatMap((update: (typeof posMasterUpdates)[number]) => [
@ -1207,13 +1214,15 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
? x.id
: x.ancestorDNA,
}));
const _orgemployeeTempPosMaster: EmployeeTempPosMaster[] = orgemployeeTempPosMaster.map((x) => ({
const _orgemployeeTempPosMaster: EmployeeTempPosMaster[] = orgemployeeTempPosMaster.map(
(x) => ({
...x,
ancestorDNA:
x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
? x.id
: x.ancestorDNA,
}));
}),
);
console.time("[AMQ] insert_employeePosMaster");
await repoEmployeePosmaster
@ -1316,7 +1325,10 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
lastUpdateFullName: "System Administrator",
lastUpdatedAt: timestamp,
});
const buildColumnData = <T extends object>(repository: Repository<T>, source: T): Partial<T> => {
const buildColumnData = <T extends object>(
repository: Repository<T>,
source: T,
): Partial<T> => {
const row = {} as Partial<T>;
const target = row as Record<string, unknown>;
const sourceRecord = source as Record<string, unknown>;
@ -1363,8 +1375,7 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
...buildColumnData(employeePositionRepository, position),
id: randomUUID(),
posMasterId: positionParentKey === "posMasterId" ? parentId : undefined,
posMasterTempId:
positionParentKey === "posMasterTempId" ? parentId : undefined,
posMasterTempId: positionParentKey === "posMasterTempId" ? parentId : undefined,
...buildAuditFields(positionTimestamp),
});
}
@ -1409,7 +1420,8 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
const dataId = x.id;
const matchedOrgRoot = findMatchedNodeByAncestorDNA(orgRootCurrent, x);
const filteredEmployeePosMaster = employeePosMasterByNode.get(getNodeKey("root", dataId)) ?? [];
const filteredEmployeePosMaster =
employeePosMasterByNode.get(getNodeKey("root", dataId)) ?? [];
await cloneEmployeeNodeBatch(
filteredEmployeePosMaster,
@ -1664,6 +1676,8 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
console.log(`[AMQ] handler_org SUCCESS - Total time: ${Date.now() - startTime}ms`);
console.timeEnd("[AMQ] handler_org_total");
await clearMenuAndRoleCache();
return true;
} catch (error) {
const totalTime = Date.now() - startTime;
@ -1683,6 +1697,32 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
}
}
async function clearMenuAndRoleCache(): Promise<void> {
const redisClient = redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
const keysAsync = promisify(redisClient.keys).bind(redisClient);
const delAsync = promisify(redisClient.del).bind(redisClient);
try {
const menuKeys = await keysAsync("menu_*");
if (menuKeys.length > 0) {
await delAsync(...menuKeys);
console.log(`[AMQ] Cleared ${menuKeys.length} menu cache keys`);
}
const roleKeys = await keysAsync("role_*");
if (roleKeys.length > 0) {
await delAsync(...roleKeys);
console.log(`[AMQ] Cleared ${roleKeys.length} role cache keys`);
}
} finally {
redisClient.quit();
}
}
async function handler_org_draft(msg: amqp.ConsumeMessage): Promise<boolean> {
const { data, token, user } = JSON.parse(msg.content.toString());
const { requestBody, request, revision } = data;

View file

@ -22,7 +22,7 @@ export function initWebSocket() {
});
io.on("connection", (ws) => {
console.log("✅ Client connected to WebSocket");
// console.log("✅ Client connected to WebSocket");
ws.on("close", () => {
console.log("❌ Client disconnected");

View file

@ -1,23 +1,37 @@
/**
*
* @param totalDays
* @returns { year, month, day }
* Normalize a duration sum using calendar arithmetic
* Converts excess days to months using average month length (30.4375 days)
* and excess months to years. Matches the logic used in stored procedures.
*
* @param years Total years from sum
* @param months Total months from sum
* @param days Total days from sum
* @returns Normalized { years, months, days }
*/
export function calculateTenure(totalDays: number) {
// 1. แปลงเป็น year เต็ม
const year = Math.floor(totalDays / 365.2524);
export function normalizeDurationSumSimple(
years: number,
months: number,
days: number,
): { years: number; months: number; days: number } {
const DAYS_PER_MONTH = 30.4375; // Average days per month in Gregorian calendar
// 2. วันที่เหลือหลังหัก year ออก
const remainAfterYear = totalDays - year * 365.2524;
let totalMonths = months;
let totalDays = days;
// 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 };
// Convert excess days to months
if (totalDays >= DAYS_PER_MONTH) {
const additionalMonths = Math.floor(totalDays / DAYS_PER_MONTH);
totalMonths += additionalMonths;
totalDays = totalDays - additionalMonths * DAYS_PER_MONTH;
}
// Convert excess months to years
let totalYears = years;
if (totalMonths >= 12) {
const additionalYears = Math.floor(totalMonths / 12);
totalYears += additionalYears;
totalMonths = totalMonths % 12;
}
return { years: totalYears, months: Math.floor(totalMonths), days: Math.floor(totalDays) };
}

View file

@ -29,6 +29,12 @@
"name": "X-API-Key",
"description": "API KEY สำหรับ Web Service",
"in": "header"
},
"internalAuth": {
"type": "apiKey",
"name": "api-key",
"description": "API KEY สำหรับ Internal Service (.NET, HRMS)",
"in": "header"
}
},
"tags": [