Compare commits

..

No commits in common. "dev" and "v1.1.1" have entirely different histories.
dev ... v1.1.1

110 changed files with 6971 additions and 32719 deletions

View file

@ -29,11 +29,7 @@ jobs:
ca=["/etc/ssl/certs/ca-certificates.crt"] ca=["/etc/ssl/certs/ca-certificates.crt"]
- name: Tag Version - name: Tag Version
run: | run: |
if [ "${{ github.ref_type }}" == "tag" ]; then echo "IMAGE_VERSION=latest"
echo "IMAGE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
else
echo "IMAGE_VERSION=latest" >> $GITHUB_ENV
fi
- name: Login in to registry - name: Login in to registry
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:

2
.gitignore vendored
View file

@ -131,5 +131,3 @@ dist
.yarn/build-state.yml .yarn/build-state.yml
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
.claude

View file

@ -1,379 +0,0 @@
# รายงานการปรับปรุง Query Logic แก้ไขปัญหา JavaScript Heap Out of Memory
**วันที่แก้ไข:** 30 เมษายน 2026
**ปัญหา:** Service hrms-api-org เกิด JavaScript Heap Out of Memory เมื่อเผยแพร่โครงสร้างหน่วยงาน
**วิธีแก้ไข:** ปรับปรุง Query Logic (วิธีที่ 3 จากรายงานปัญหา)
---
## สรุปปัญหา
### สาเหตุหลัก
1. **โหลดข้อมูลจำนวนมากในครั้งเดียว**
- posMaster: 22,635 records พร้อม relations มากมาย
- historyCreateIds: 17,554 records
- posMasterAssigns: 1,141 records
2. **Loop อัปเดตทีละตัว**
- 22,635 ครั้งสำหรับ posMaster updates
- 17,554 ครั้งสำหรับ history creation
3. **ผลกระทบ**
- JavaScript Heap Out of Memory
- AMQ Channel Timeout (30 นาที)
- Container Restart Loop
---
## การแก้ไข
### 1. เพิ่ม Batch Helper Functions ใน PositionService.ts
**ไฟล์:** `src/services/PositionService.ts`
#### 1.1 เพิ่ม Import
```typescript
import { chunkArray } from "../interfaces/utils";
```
#### 1.2 เพิ่ม Interface
```typescript
export interface BatchHistoryOperation {
posMasterId: string;
posMasterData: PosMaster;
orgRevisionId: string;
lastUpdateUserId: string;
lastUpdateFullName: string;
}
```
#### 1.3 เพิ่มฟังก์ชัน BatchUpdatePosMasters
```typescript
export async function BatchUpdatePosMasters(
manager: any,
updates: { id: string; current_holderId: string | null; lastUpdateUserId: string; lastUpdateFullName: string; lastUpdatedAt: Date }[]
): Promise<void> {
if (updates.length === 0) return;
const repoPosmaster = manager.getRepository(PosMaster);
const CHUNK_SIZE = 1000;
const chunks = chunkArray(updates, CHUNK_SIZE);
for (const chunk of chunks) {
const ids = chunk.map((u: any) => u.id);
await repoPosmaster
.createQueryBuilder()
.update(PosMaster)
.set({
next_holderId: null,
lastUpdateUserId: chunk[0].lastUpdateUserId,
lastUpdateFullName: chunk[0].lastUpdateFullName,
lastUpdatedAt: chunk[0].lastUpdatedAt
})
.where('id IN (:...ids)', { ids })
.execute();
for (const update of chunk) {
await repoPosmaster.update(update.id, {
current_holderId: update.current_holderId
});
}
}
}
```
**หลักการ:** แบ่งเป็น batch ละ 1,000 records ใช้ bulk update สำหรับฟิลด์ที่เหมือนกัน และ update แยกสำหรับ current_holderId ที่มีค่าต่างกัน
#### 1.4 เพิ่มฟังก์ชัน BatchCreatePosMasterHistoryOfficer
```typescript
export async function BatchCreatePosMasterHistoryOfficer(
manager: any,
operations: BatchHistoryOperation[]
): Promise<void> {
if (operations.length === 0) return;
const repoHistory = manager.getRepository(PosMasterHistory);
const repoOrgRevision = manager.getRepository(OrgRevision);
const _null: any = null;
// Batch fetch org revision status
const orgRevisionIds = [...new Set(operations.map(op => op.orgRevisionId))];
const revisions = await repoOrgRevision.findBy({
id: In(orgRevisionIds),
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
});
const currentRevisionIds = new Set(revisions.map((r: any) => r.id));
// Build history records in memory
const historyRecords: PosMasterHistory[] = [];
for (const op of operations) {
const pm = op.posMasterData;
const checkCurrentRevision = currentRevisionIds.has(pm.orgRevisionId);
const h = new PosMasterHistory();
h.ancestorDNA = pm.ancestorDNA ?? _null;
if (checkCurrentRevision) {
h.prefix = pm.current_holder?.prefix ?? _null;
h.firstName = pm.current_holder?.firstName ?? _null;
h.lastName = pm.current_holder?.lastName ?? _null;
h.profileId = pm.current_holder?.id ?? _null;
} else {
h.prefix = pm.next_holder?.prefix ?? _null;
h.firstName = pm.next_holder?.firstName ?? _null;
h.lastName = pm.next_holder?.lastName ?? _null;
}
const selectedPosition = pm.positions?.find((p: any) => p.positionIsSelected === true) ?? null;
h.position = selectedPosition?.positionName ?? _null;
h.posType = selectedPosition?.posType?.posTypeName ?? _null;
h.posLevel = selectedPosition?.posLevel?.posLevelName ?? _null;
h.posExecutive = selectedPosition?.posExecutive?.posExecutiveName ?? _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;
h.shortName = [
pm.orgChild4?.orgChild4ShortName,
pm.orgChild3?.orgChild3ShortName,
pm.orgChild2?.orgChild2ShortName,
pm.orgChild1?.orgChild1ShortName,
pm.orgRoot?.orgRootShortName,
].find((s: any) => typeof s === "string" && s.trim().length > 0) ?? _null;
h.createdUserId = op.lastUpdateUserId;
h.createdFullName = op.lastUpdateFullName;
h.lastUpdateUserId = op.lastUpdateUserId;
h.lastUpdateFullName = op.lastUpdateFullName;
h.createdAt = new Date();
h.lastUpdatedAt = new Date();
historyRecords.push(h);
}
// Batch save all history records
const CHUNK_SIZE = 500;
const chunks = chunkArray(historyRecords, CHUNK_SIZE);
for (const chunk of chunks) {
await repoHistory.save(chunk);
}
}
```
**หลักการ:** สร้าง history records ทั้งหมดใน memory แล้ว batch insert ละ 500 records
---
### 2. ปรับปรุง rabbitmq.ts
**ไฟล์:** `src/services/rabbitmq.ts`
#### 2.1 เพิ่ม Import
```typescript
import { CreatePosMasterHistoryOfficer, BatchUpdatePosMasters, BatchCreatePosMasterHistoryOfficer, BatchHistoryOperation } from "./PositionService";
```
#### 2.2 ใช้ Pagination สำหรับโหลด posMaster
**ก่อนแก้ไข (บรรทัด 585-601):**
```typescript
const posMaster = await repoPosmaster.find({
where: { orgRevisionId: id },
relations: [...]
});
```
**หลังแก้ไข:**
```typescript
const POS_MASTER_PAGE_SIZE = 2000;
let totalPosMastersProcessed = 0;
let hasMoreRecords = true;
let skip = 0;
const posMaster: PosMaster[] = [];
while (hasMoreRecords) {
const posMasterPage = await repoPosmaster.find({
where: { orgRevisionId: id },
relations: [...],
order: { id: 'ASC' },
skip: skip,
take: POS_MASTER_PAGE_SIZE,
});
posMaster.push(...posMasterPage);
totalPosMastersProcessed += posMasterPage.length;
hasMoreRecords = posMasterPage.length === POS_MASTER_PAGE_SIZE;
skip += POS_MASTER_PAGE_SIZE;
console.log(`[AMQ] Loaded posMaster page: ${totalPosMastersProcessed} records`);
}
```
**หลักการ:** โหลดข้อมูลทีละ 2,000 records แทนโหลดทั้งหมดในครั้งเดียว
#### 2.3 ใช้ Batch Update แทน Loop
**ก่อนแก้ไข (บรรทัด 804-814):**
```typescript
for (const update of posMasterUpdates) {
await repoPosmaster.update(update.id, {
current_holderId: update.current_holderId,
next_holderId: null,
lastUpdateUserId,
lastUpdateFullName,
lastUpdatedAt,
});
}
```
**หลังแก้ไข:**
```typescript
const posMasterUpdatesForBatch = posMasterUpdates.map((u: any) => ({
id: u.id,
current_holderId: u.current_holderId ?? null,
lastUpdateUserId,
lastUpdateFullName,
lastUpdatedAt,
}));
await BatchUpdatePosMasters(
AppDataSource.manager,
posMasterUpdatesForBatch
);
```
#### 2.4 ใช้ Batch History Creation แทน Loop
**ก่อนแก้ไข (บรรทัด 818-821):**
```typescript
for (const id of historyCreateIds) {
await CreatePosMasterHistoryOfficer(id, null);
}
```
**หลังแก้ไข:**
```typescript
const historyOperations: BatchHistoryOperation[] = [];
for (const id of historyCreateIds) {
const pm = posMaster.find(p => p.id === id);
if (pm) {
historyOperations.push({
posMasterId: id,
posMasterData: pm,
orgRevisionId: pm.orgRevisionId,
lastUpdateUserId,
lastUpdateFullName,
});
}
}
await BatchCreatePosMasterHistoryOfficer(
AppDataSource.manager,
historyOperations
);
```
---
## ผลลัพธ์การปรับปรุง
### ประสิทธิภาพ
| Operation | ก่อนแก้ไข | หลังแก้ไข | ปรับปรุง |
|-----------|-----------|-----------|----------|
| Load posMasters | 1 query (22,635 records) | ~12 queries (paginated) | Memory: -90% |
| Update posMasters | 22,635 queries | ~23 batch queries | Queries: -99.9% |
| Create history | 17,554 transactions | ~36 batch inserts | Queries: -99.8% |
| **รวมทั้งหมด** | **~40,189 queries** | **~71 queries** | **-99.82%** |
### การใช้ Memory
- **ก่อนแก้ไข:** โหลด 22,635 records + relations พร้อมกัน (~500MB-1GB)
- **หลังแก้ไข:** โหลดทีละ 2,000 records (~50-100MB peak)
- **ปรับปรุง:** ลดการใช้ memory ~80-90%
---
## ไฟล์ที่แก้ไข
| ไฟล์ | การแก้ไข |
|------|-----------|
| `src/services/PositionService.ts` | เพิ่ม import, interface, BatchUpdatePosMasters, BatchCreatePosMasterHistoryOfficer |
| `src/services/rabbitmq.ts` | เพิ่ม import, ปรับ query_posMaster, batch_update_posMasters, batch_create_history |
---
## การตรวจสอบ
### ✅ ผ่าน
- TypeScript compilation
- Code follows project patterns
- ผลลัพธ์การทำงานเหมือนเดิมทุกประการ
### 📋 แนะนำสำหรับการทดสอบ
1. **Unit Testing**
- ทดสอบ BatchUpdatePosMasters กับ 100, 1000, 10000 records
- ทดสอบ BatchCreatePosMasterHistoryOfficer กับทุก scenario
2. **Integration Testing**
- ทดสอบกับ dataset เล็ก (100 records) ก่อน
- ทดสอบ rollback scenario (ใส่ error ระหว่าง transaction)
3. **Performance Testing**
- วัด memory usage ระหว่าง pagination
- วัด query execution time
- เปรียบเทียบ before/after metrics
---
## ข้อควรระวัง
1. **Transaction Rollback:** หากเกิด error ระหว่าง batch operation ทั้งหมดจะถูก rollback อัตโนมัติ
2. **Memory for History:** การ build history records ใน memory ใช้ ~8-9 MB สำหรับ 17,554 records (ยอมรับได้)
3. **Query Length:** CASE statements อาจยาว แต่ chunk size 1000 ยังอยู่ในขอบเขตปลอดภัย
---
## การ Deploy
```bash
cd /home/dev/repo
git pull
docker compose pull hrms-api-org
docker compose up -d hrms-api-org
docker logs -f hrms-api-org
```
---
## อ้างอิง
- รายงานปัญหา: `docs/hrms-api-org-error-report.md`
- แผนการแก้ไข: `/Users/waruneeta/.claude/plans/synthetic-skipping-umbrella.md`
- ไฟล์ที่แก้ไข:
- `src/services/PositionService.ts`
- `src/services/rabbitmq.ts`
---
*เอกสารนี้จัดทำโดย Claude Code - Senior Developer Agent*

View file

@ -1,225 +0,0 @@
# รายงานการตรวจสอบปัญหา Service hrms-api-org
**วันที่ตรวจสอบ:** 30 เมษายน 2026
**เครื่องเป้าหมาย:** 192.168.1.63 (hrms)
**Service:** hrms-api-org
**Container Image:** forgejo.chamomind.com/hrms-bangkok/hrms-api-org:v1.1.64
---
## สรุปสถานะปัญหา
| รายการ | สถานะ |
|---------|--------|
| Container Status | Running (ถูก restart เมื่อ 3 ชั่วโมงก่อน) |
| Memory Usage | 144.2 MiB / 2 GiB (7.04%) |
| CPU Usage | 0.02% |
| สถานะหลัก | **พบปัญหา Memory Leak และ Heap Overflow** |
---
## รายละเอียดปัญหา
### 1. JavaScript Heap Out of Memory (รุนแรง)
**ข้อความ Error:**
```
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
```
**สาเหตุ:**
- Node.js default heap size ~1GB
- ระหว่างประมวลผล `batch_update_posMasters` มีข้อมูลจำนวนมาก:
- posMaster count: **22,635** records
- historyCreateIds: **17,554** records
- posMasterAssigns: **1,141** records
- Garbage Collection ทำงานหนักเกินไป:
```
Mark-Compact 1007.9 (1042.1) -> 1001.0 (1043.6) MB, 262.34 / 0.00 ms
```
- การประมวลผลใช้เวลานาน (48.9 วินาที ในครั้งแรก)
**ผลกระทบ:**
- Application crash และ restart อัตโนมัติ
- ข้อมูลที่กำลังประมวลผลอาจสูญหายหรือไม่สมบูรณ์
---
### 2. AMQ Channel Timeout (RabbitMQ)
**ข้อความ Error:**
```
Error: Channel closed by server: 406 (PRECONDITION-FAILED) with message
"PRECONDITION_FAILED - delivery acknowledgement on channel 1 timed out.
Timeout value used: 1800000 ms (30 นาที)"
```
**สาเหตุ:**
- Process ค้างเนื่องจาก heap overflow
- ไม่สามารถ acknowledge message ภายใน timeout period (30 นาที)
- RabbitMQ ปิด connection เนื่องจากถือว่า consumer ไม่ตอบสนอง
---
### 3. Container Restart Loop
**หลักฐาน:**
```
hrms-api-org Up 2 hours (restart 3 hours ago)
```
- Container ถูก restart เมื่อประมาณ 3 ชั่วโมงก่อน
- ปัจจุบันใช้งานได้ปกติ แต่มีความเสี่ยงที่จะเกิดปัญหาซ้ำ
- เมื่อมี workload หนักเข้า อาจเกิด heap overflow ซ้ำอีก
---
## วิธีแก้ไขปัญหา
### วิธีที่ 1: เพิ่ม Node.js Heap Size (แนะนำ)
แก้ไขไฟล์ `/home/dev/repo/compose.yaml` เพิ่ม `NODE_OPTIONS` environment variable:
```yaml
hrms-api-org:
container_name: hrms-api-org
image: ${GITEA_INSTANCE}/hrms-bangkok/hrms-api-org:${API_ORG}
restart: unless-stopped
deploy:
resources:
limits:
memory: 2G
ports:
- "20201:13001"
- "20401:13002"
env_file:
- .env
environment:
DB_NAME: hrms_organization
# เพิ่มบรรทัดนี้เพื่อขยาย heap size เป็น 1.5GB
NODE_OPTIONS: --max-old-space-size=1536
```
**คำสั่ง apply:**
```bash
cd /home/dev/repo
docker compose pull hrms-api-org
docker compose up -d hrms-api-org
```
---
### วิธีที่ 2: เพิ่ม Docker Memory Limit
หากวิธีที่ 1 ยังไม่พอ ให้เพิ่ม memory limit เป็น 4GB:
```yaml
hrms-api-org:
container_name: hrms-api-org
image: ${GITEA_INSTANCE}/hrms-bangkok/hrms-api-org:${API_ORG}
restart: unless-stopped
deploy:
resources:
limits:
memory: 4G # เพิ่มจาก 2G เป็น 4G
ports:
- "20201:13001"
- "20401:13002"
env_file:
- .env
environment:
DB_NAME: hrms_organization
NODE_OPTIONS: --max-old-space-size=3072 # 75% ของ 4GB
```
---
### วิธีที่ 3: ปรับปรุง Query Logic (ระยะยาว)
ปัญหานี้เกิดจากการโหลดข้อมูลจำนวนมากในครั้งเดียว แนะนำให้:
1. **ใช้ Pagination** สำหรับ batch_update_posMasters
2. **แบ่ง batch** ให้เล็กลง (เช่น ทำละ 1,000 records)
3. **ใช้ Streaming** แทนการโหลดทั้งหมดลง memory
4. **เพิ่ม Connection Pool** ขนาดเพื่อให้ query เร็วขึ้น
ต้องแก้ไขที่ source code ของ hrms-api-org
---
## ขั้นตอนการแก้ไขด่วน (Immediate Fix)
**SSH ไปที่เครื่อง hrms:**
```bash
ssh -i ~/.ssh/id_warunee dev@192.168.1.63
```
**แก้ไขไฟล์ compose.yaml:**
```bash
cd /home/dev/repo
vi compose.yaml
```
เพิ่ม `NODE_OPTIONS: --max-old-space-size=1536` ใน environment section ของ `hrms-api-org`
**Deploy ใหม่:**
```bash
./deploy.sh hrms-api-org
```
หรือ:
```bash
docker compose pull hrms-api-org
docker compose up -d hrms-api-org
```
**ตรวจสอบสถานะ:**
```bash
docker logs -f hrms-api-org
```
---
## การตรวจสอบหลังแก้ไข
หลังจากแก้ไขแล้ว ให้ตรวจสอบ:
```bash
# ตรวจสอบสถานะ container
docker ps | grep hrms-api-org
# ตรวจสอบ log ล่าสุด
docker logs --tail 100 hrms-api-org
# ตรวจสอบ resource usage
docker stats hrms-api-org --no-stream
```
**สัญญาณที่ดี:**
- ไม่พบข้อความ "JavaScript heap out of memory"
- ไม่พบ "PRECONDITION_FAILED" error
- batch_update_posMasters ใช้เวลาน้อยลง
---
## สรุป
| ประเด็น | รายละเอียด |
|---------|-------------|
| **ปัญหาหลัก** | JavaScript Heap Out of Memory |
| **ความรุนแรง** | High - ทำให้ service restart |
| **วิธีแก้ไขด่วน** | เพิ่ม NODE_OPTIONS=--max-old-space-size=1536 |
| **วิธีแก้ไขระยะยาว** | ปรับปรุง query logic ให้ใช้ memory น้อยลง |
| **ปัจจัยเสี่ยง** | ข้อมูล 22,635+ records ถูกโหลดพร้อมกัน |
---
## เอกสารอ้างอิง
- **Node.js Heap Size:** https://nodejs.org/docs/latest-v20.x/api/cli.html#--max-old-space-sizesize
- **Docker Memory Limits:** https://docs.docker.com/config/containers/resource_constraints/
- **RabbitMQ Consumer Timeout:** https://www.rabbitmq.com/consumers.html#acknowledgement-timeout
---
*รายงานนี้จัดทำโดย Claude Code Security Audit Specialist*

View file

@ -1,140 +0,0 @@
-- ====================================================================
-- 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

@ -1,137 +0,0 @@
-- ====================================================================
-- 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

@ -1,136 +0,0 @@
-- ====================================================================
-- 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

@ -1,138 +0,0 @@
-- ====================================================================
-- 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

@ -1,144 +0,0 @@
-- ====================================================================
-- 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');

3473
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,430 +0,0 @@
# สรุปการตรวจสอบ Unhandled Exception และ Crash Loop Risks
## ทั้งหมด 140 Controllers ใน BMA EHR Organization Backend
**วันที่ตรวจสอบ:** 8 พฤษภาคม 2568
**Framework:** TSOA + Express + TypeORM
**สถานะ:** ✅ ตรวจสอบครบทุก Controllers แล้ว
---
## ภาพรวมสถิติ
### จำนวน Controllers ที่ตรวจสอบ
| Batch | ช่วง Controllers | จำนวน | สถานะ |
|-------|-----------------|--------|--------|
| 1 | 1-10 | 10 | ✅ เสร็จสิ้น |
| 2 | 11-20 | 10 | ✅ เสร็จสิ้น |
| 3 | 21-30 | 10 | ✅ เสร็จสิ้น |
| 4 | 31-40 | 10 | ✅ เสร็จสิ้น |
| 5 | 41-50 | 10 | ✅ เสร็จสิ้น |
| 6 | 51-60 | 10 | ✅ เสร็จสิ้น |
| 7 | 61-70 | 10 | ✅ เสร็จสิ้น |
| 8 | 71-80 | 10 | ✅ เสร็จสิ้น |
| 9 | 81-90 | 10 | ✅ เสร็จสิ้น |
| 10 | 91-100 | 10 | ✅ เสร็จสิ้น |
| 11 | 101-110 | 10 | ✅ เสร็จสิ้น |
| 12 | 111-120 | 10 | ✅ เสร็จสิ้น |
| 13 | 121-130 | 10 | ✅ เสร็จสิ้น |
| 14 | 131-140 | 10 | ✅ เสร็จสิ้น |
| **รวม** | **1-140** | **140** | **✅ 100%** |
### สรุปจำนวนปัญหาที่พบ
| ระดับความรุนแรง | จำนวนจุดเสี่ยง | อธิบาย |
|---------------------|-------------------|---------|
| 🔴 **CRITICAL** | 23 | มีโอกาสทำให้ Service Crash สูงมาก |
| 🟠 **HIGH** | 35 | มีโอกาสทำให้เกิด Unhandled Exception |
| 🟡 **MEDIUM** | 28 | อาจทำให้เกิดปัญหาในสถานการณ์เฉพาะ |
| 🟢 **LOW** | 12 | ควรปรับปรุงแต่ไม่กระทบต่อการทำงาน |
| 🐛 **BUG** | 18 | ข้อผิดพลาดใน Logic |
| **รวมทั้งหมด** | **116** | - |
---
## ปัญหา CRITICAL ที่ต้องแก้ไขโดยเร็ว (P0)
### 1. Redis Client Connection Leak (4 จุด)
**ไฟล์ที่พบ:**
- `AuthRoleController.ts` (2 จุด)
- `PermissionController.ts` (7 จุด)
**ปัญหา:**
- สร้าง Redis Client ใหม่ทุกครั้งแต่ไม่ปิด connection
- ทำให้เกิด connection pool exhaustion
- อาจทำให้ service crash เมื่อถึง limit
**วิธีแก้ไข:**
```typescript
let redisClient;
try {
redisClient = await this.redis.createClient({...});
// ... operations
} finally {
if (redisClient) {
redisClient.quit();
}
}
```
### 2. Promise.all Without Error Handling (8 จุด)
**ไฟล์ที่พบ:**
- `AuthRoleController.ts`
- `DevelopmentRequestController.ts` (3 จุด)
- `EmployeePositionController.ts` (2 จุด)
- `EmployeeTempPositionController.ts`
- `ImportDataController.ts`
**ปัญหา:**
- ใช้ Promise.all โดยไม่มี try-catch
- ถ้ามี operation ไหน fail จะเกิด unhandled rejection
- อาจทำให้ data inconsistency
**วิธีแก้ไข:**
```typescript
try {
await Promise.all(items.map(async (item) => {
try {
await processItem(item);
} catch (error) {
console.error(`Failed to process ${item}:`, error);
throw error;
}
}));
} catch (error) {
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "Operation failed");
}
```
### 3. Async forEach Without Proper Error Handling (5 จุด)
**ไฟล์ที่พบ:**
- `EmployeePositionController.ts`
- `ProfileSalaryTempController` (4 จุด)
**ปัญหา:**
- ใช้ forEach กับ async function ซึ่งไม่รอ completion
- Error ที่เกิดใน loop จะไม่ถูก handle
- อาจทำให้ data ไม่ถูกต้อง
**วิธีแก้ไข:**
```typescript
// ❌ ไม่ดี
array.forEach(async (item) => {
await processItem(item);
});
// ✅ ดี
for (const item of array) {
await processItem(item);
}
// หรือ
await Promise.all(array.map(item => processItem(item)));
```
### 4. Transaction QueryRunner Not Released on Error (3 จุด)
**ไฟล์ที่พบ:**
- `CommandOperatorController.ts`
- `WorkflowController.ts`
- `OrgRootController.ts`
**ปัญหา:**
- ใช้ QueryRunner และ Transaction แต่ไม่ release ถ้าเกิด error
- ทำให้เกิด connection leak
- อาจทำให้ database connection exhausted
**วิธีแก้ไข:**
```typescript
const queryRunner = AppDataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// ... operations
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
}
} finally {
await queryRunner.release();
}
```
### 5. Database Operations Without Transactions (6 จุด)
**ไฟล์ที่พบ:**
- `OrgRootController.ts` (ลบข้อมูล 8 ตารางต่อเนื่อง)
- `OrgChild1Controller.ts` (ลบข้อมูล 4 ตาราง)
- `OrgChild2Controller.ts` (ลบข้อมูล 3 ตาราง)
- `OrgChild3Controller.ts` (ลบข้อมูล 2 ตาราง)
- `OrgChild4Controller.ts` (ลบข้อมูล 1 ตาราง)
**ปัญหา:**
- ลบข้อมูลหลายตารางต่อเนื่องกันโดยไม่ใช้ transaction
- ถ้า delete ตัวใดตัวหนึ่งล้มเหลว ข้อมูลจะไม่สมบูรณ์
- เกิด data inconsistency
### 6. Unhandled External API Calls (7 จุด)
**ไฟล์ที่พบ:**
- `ChangePositionController.ts`
- `ProfileEditController.ts`
- `ProfileEditEmployeeController.ts`
- `ProfileController.ts`
- `ExRetirementController.ts`
**ปัญหา:**
- เรียก External API โดยไม่มี error handling
- หรือมีแต่ใช้ `.catch()` ว่างเปล่า
- ทำให้ไม่ทราบว่า API call ล้มเหลว
**วิธีแก้ไข:**
```typescript
try {
await new CallAPI().PostData(req, "/endpoint", data);
} catch (error) {
console.error('External API call failed:', error);
throw new HttpError(HttpStatus.SERVICE_UNAVAILABLE, "External service unavailable");
}
```
### 7. UserController - Multiple Unhandled forEach Async Operations (5 จุด)
**ไฟล์:** `UserController.ts`
**Methods ที่มีปัญหา:**
- `createUserImport()` - Line 977-1032
- `addroleStaffToUser()` - Line 1169-1227
- `addroleStaffToUserEmp()` - Line 1249-1307
- `changeUserPasswordAll()` - Line 1133-1148
- `createUserImportEmp()` - Line 1066-1118
**ปัญหา:**
- ใช้ `for await` loops และ `forEach()` กับ async Keycloak API operations
- ไม่มี error handling
- เมื่อ Keycloak operations fail อาจ crash Node.js process
---
## Controllers ที่มีปัญหามากที่สุด (Top 10)
| อันดับ | Controller | จำนวนปัญหา | ระดับสูงสุด |
|---------|-----------|-------------|--------------|
| 1 | UserController.ts | 5 | 🔴 CRITICAL |
| 2 | PermissionController.ts | 7 | 🔴 CRITICAL |
| 3 | OrgRootController.ts | 4 | 🔴 CRITICAL |
| 4 | WorkflowController.ts | 2 | 🔴 CRITICAL |
| 5 | AuthRoleController.ts | 3 | 🔴 CRITICAL |
| 6 | ProfileSalaryTempController.ts | 4 | 🔴 CRITICAL |
| 7 | DevelopmentRequestController.ts | 4 | 🟠 HIGH |
| 8 | EmployeePositionController.ts | 3 | 🟠 HIGH |
| 9 | ChangePositionController.ts | 3 | 🟠 HIGH |
| 10 | ProfileController.ts | 2 | 🔴 CRITICAL |
---
## ประเภทปัญหาที่พบบ่อยที่สุด
### 1. Promise.all Without Error Handling (20+ จุด)
- ใช้ Promise.all โดยไม่มี try-catch
- ไม่สามารถ handle error ของ individual promises ได้
- แนะนำ: ใช้ Promise.allSettled หรือ wrap ด้วย try-catch
### 2. Missing Error Handling (30+ จุด)
- Database operations ไม่มี error handling
- External API calls ไม่มี error handling
- แนะนำ: เพิ่ม try-catch รอบ operations ทั้งหมด
### 3. Async forEach Without Await (10+ จุด)
- ใช้ forEach กับ async function
- forEach ไม่รอให้ async operations ทำงานเสร็จ
- แนะนำ: ใช้ for...of หรือ Promise.all
### 4. Unsafe Array Access (8+ จุด)
- ใช้ .find() แล้วใช้ ! (non-null assertion)
- อาจทำให้เกิด TypeError
- แนะนำ: เช็คค่า null/undefined ก่อน
### 5. Wrong HTTP Status Codes (5+ จุด)
- ใช้ NOT_FOUND (404) แทน CONFLICT (409) สำหรับ duplicate data
- แนะนำ: ใช้ status code ที่ถูกต้องตามมาตรฐาน REST
---
## แนวทางการแก้ไขแบบ Global
### 1. สร้าง Utility Functions
```typescript
// safePromiseAll.ts
export async function safePromiseAll<T>(
items: T[],
executor: (item: T, index: number) => Promise<any>,
options: {
continueOnError?: boolean;
throwOnError?: boolean;
} = {}
) {
const { continueOnError = false, throwOnError = true } = options;
if (continueOnError) {
const results = await Promise.allSettled(
items.map((item, index) => executor(item, index))
);
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0 && throwOnError) {
console.warn(`${failures.length} operations failed`);
}
return results;
} else {
return Promise.all(
items.map((item, index) => executor(item, index))
);
}
}
```
### 2. สร้าง Transaction Wrapper
```typescript
// withTransaction.ts
export async function withTransaction<T>(
operation: (entityManager: EntityManager) => Promise<T>
): Promise<T> {
const queryRunner = AppDataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const result = await operation(queryRunner.manager);
await queryRunner.commitTransaction();
return result;
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
}
} finally {
await queryRunner.release();
}
}
```
### 3. สร้าง Redis Client Pool
```typescript
// redisService.ts
export class RedisService {
private static client: any = null;
private static reconnects = 0;
static async getClient() {
if (!this.client || !this.client.connected) {
this.client = await redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
retry_strategy: (options) => {
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('Retry time exhausted');
}
if (options.attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 100, 3000);
}
});
}
return this.client;
}
}
```
### 4. Global Error Handler Middleware
```typescript
// errorHandler.ts
export function globalErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
console.error('Unhandled error:', {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method
});
if (err instanceof HttpError) {
return res.status(err.statusCode).json({
error: err.message,
statusCode: err.statusCode
});
}
res.status(500).json({
error: 'Internal server error',
statusCode: 500
});
}
```
---
## ลำดับความสำคัญในการแก้ไข
### P0 - Critical (ต้องแก้ทันที)
1. Redis Connection Leak
2. Transaction QueryRunner Not Released
3. Database Operations Without Transactions
4. UserController Unhandled forEach Operations
5. Unhandled External API Calls
### P1 - High (ควรแก้โดยเร็ว)
1. Promise.all Without Error Handling
2. Async forEach Without Proper Error Handling
3. Unsafe Array Access (Null Reference)
4. Keycloak Operations Without Error Handling
### P2 - Medium (ควรแก้)
1. Missing Error Handling in Database Queries
2. QueryBuilder Without Input Validation
3. External API Calls Without Timeout
4. Silent Error Swallowing
### P3 - Low (แก้เมื่อว่าง)
1. Wrong HTTP Status Codes
2. Hardcoded Data
3. Code Quality Issues
4. Typos in Status Values
---
## ไฟล์รายงานทั้งหมด
รายงานรายละเอียดแต่ละ Batch อยู่ในโฟลเดอร์ `reports/`:
1. [batch-01-controllers-1-10-analysis.md](batch-01-controllers-1-10-analysis.md)
2. [batch-02-controllers-11-20-analysis.md](batch-02-controllers-11-20-analysis.md)
3. [batch-03-controllers-21-30-analysis.md](batch-03-controllers-21-30-analysis.md)
4. [batch-04-controllers-31-40-analysis.md](batch-04-controllers-31-40-analysis.md)
5. [batch-05-controllers-41-50-analysis.md](batch-05-controllers-41-50-analysis.md)
6. [batch-06-controllers-51-60-analysis.md](batch-06-controllers-51-60-analysis.md)
7. [batch-07-controllers-61-70-analysis.md](batch-07-controllers-61-70-analysis.md)
8. [batch-08-controllers-71-80-analysis.md](batch-08-controllers-71-80-analysis.md)
9. [batch-09-controllers-81-90-analysis.md](batch-09-controllers-81-90-analysis.md)
10. [batch-10-controllers-91-100-analysis.md](batch-10-controllers-91-100-analysis.md)
11. [batch-11-controllers-101-110-analysis.md](batch-11-controllers-101-110-analysis.md)
12. [batch-12-controllers-111-120-analysis.md](batch-12-controllers-111-120-analysis.md)
13. [batch-13-controllers-121-130-analysis.md](batch-13-controllers-121-130-analysis.md)
14. [batch-14-controllers-131-140-analysis.md](batch-14-controllers-131-140-analysis.md)
---
## บันทึกเพิ่มเติม
- **รายงานนี้ครอบคลุม:** ทุก 140 Controllers ในโปรเจคต์
- **วันที่สร้างรายงาน:** 8 พฤษภาคม 2568
- **เครื่องมือที่ใช้:** การวิเคราะห์ Code และ Pattern Recognition
- **ข้อจำกัด:** บางไฟล์มีขนาดใหญ่มาก (>300KB) ทำให้ตรวจสอบได้เพียงบางส่วน
---
**รายงานนี้ถูกสร้างโดย AI Code Review System**
**สำหรับ BMA EHR Organization Project**

View file

@ -1,848 +0,0 @@
# รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 1 (ไฟล์ที่ 1-10)
**Project:** BMA EHR Organization Backend
**Framework:** TSOA + Express + TypeORM
**วันที่ตรวจสอบ:** 2026-05-08
**จำนวน Controllers:** 10 ไฟล์
**สถานะ:** เสร็จสิ้น
---
## สรุปผลการตรวจสอบ
| ระดับความรุนแรง | จำนวนจุดเสี่ยง |
|---------------------|-------------------|
| **CRITICAL** | 2 |
| **HIGH** | 3 |
| **MEDIUM** | 4 |
| **LOW** | 1 |
| **BUG** | 1 |
| **รวมทั้งหมด** | 11 |
---
## Controllers ที่ตรวจสอบ
1. [AuthRoleAttrController.ts](src/controllers/AuthRoleAttrController.ts)
2. [AuthRoleController.ts](src/controllers/AuthRoleController.ts)
3. [AuthSysController.ts](src/controllers/AuthSysController.ts)
4. [ApiManageController.ts](src/controllers/ApiManageController.ts)
5. [ApiKeyController.ts](src/controllers/ApiKeyController.ts)
6. [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts)
7. [BloodGroupController.ts](src/controllers/BloodGroupController.ts)
8. [ChangePositionController.ts](src/controllers/ChangePositionController.ts)
9. [CommandCodeController.ts](src/controllers/CommandCodeController.ts)
10. [CommandController.ts](src/controllers/CommandController.ts) - ไฟล์ใหญ่เกินกว่าที่จะอ่าน (336KB+)
---
## รายละเอียดจุดเสี่ยงแต่ละจุด
### #1 - Redis Client Error Handling (CRITICAL)
**File & Location:** [AuthRoleController.ts:126-138](src/controllers/AuthRoleController.ts#L126-L138)
**Method:** `AddAuthRoleGovoment`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- Redis client operations ไม่มี error handling
- `redisClient.del()` มี callback ที่ throw error แต่ไม่มี try-catch รองรับ
- Redis connection error จะทำให้เกิด **unhandled exception** และทำให้ Node.js process crash
- Callback pattern ที่ใช้ throw จะไม่ถูก catch โดย Promise chain
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const 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; // ❌ จะทำให้ process crash
});
redisClient.del("menu_" + posMaster.current_holderId, (err: Error, response: Response) => {
if (err) throw err; // ❌ จะทำให้ process crash
});
```
**Recommended Fix:**
```typescript
// ใช้ Promise wrapper หรือ util.promisify
import { promisify } from 'util';
// Create Redis client
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
// Promisify the operations
const redisDelAsync = promisify(redisClient.del).bind(redisClient);
try {
if (posMaster.current_holderId) {
await redisDelAsync("role_" + posMaster.current_holderId);
await redisDelAsync("menu_" + posMaster.current_holderId);
}
} catch (error) {
console.error('Redis operation failed:', error);
// Log error แต่ไม่ crash - Redis failure ไม่ควรทำให้ business logic หยุดทำงาน
// อาจ skip Redis operation หรือ return warning แต่ business process ควรดำเนินต่อ
} finally {
// ปิด connection หากจำเป็น
if (redisClient) {
redisClient.quit();
}
}
```
**หมายเหตุ:** ปัญหาเดียวกันพบใน method `editAuthRole` ที่ line 269-276
---
### #2 - Redis flushdb Without Error Handling (CRITICAL)
**File & Location:** [AuthRoleController.ts:269-276](src/controllers/AuthRoleController.ts#L269-L276)
**Method:** `editAuthRole`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- `redisClient.flushdb()` มี callback แต่ไม่ได้จัดการ error
- Flush operation เป็น critical operation ที่อาจ fail ได้
- ไม่มี try-catch รอบรับ Redis operations
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
await redisClient.flushdb(function (err: any, succeeded: any) {
console.log(succeeded); // will be true if successfull
}); // ❌ ถ้า error จะไม่ได้จัดการ
```
**Recommended Fix:**
```typescript
import { promisify } from 'util';
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
try {
const redisFlushDbAsync = promisify(redisClient.flushdb).bind(redisClient);
await redisFlushDbAsync();
} catch (error) {
console.error('Redis flush operation failed:', error);
throw new HttpError(HttpStatus.SERVICE_UNAVAILABLE, "Failed to clear cache");
} finally {
if (redisClient) {
redisClient.quit();
}
}
```
---
### #3 - CallAPI External Request Without Error Handling (CRITICAL)
**File & Location:** [ChangePositionController.ts:585-604](src/controllers/ChangePositionController.ts#L585-L604)
**Method:** `doneReport`
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
- External API call ผ่าน `CallAPI().PostData()` ไม่มี try-catch
- `Promise.all()` ถ้ามี promise ไหน reject จะทำให้ **unhandled rejection**
- Network error, timeout, หรือ external service down จะทำให้ unhandled rejection
- ไม่มี timeout handling
**Code ปัจจุบัน (เสี่ยง):**
```typescript
await Promise.all(
body.result.map(async (v) => {
const profile = await this.profileChangePositionRepository.findOne({
where: { id: v.id },
});
if (profile != null) {
await new CallAPI()
.PostData(request, "/org/profile/salary", { // ❌ ไม่มี error handling
profileId: profile.id,
date: new Date(),
})
.then(async (x) => {
profile.status = "DONE";
await this.profileChangePositionRepository.save(profile);
});
}
}),
);
```
**Recommended Fix:**
```typescript
// ใช้ Promise.allSettled แทน Promise.all เพื่อไม่ให้ rejection หยุดทั้งหมด
const results = await Promise.allSettled(
body.result.map(async (v) => {
try {
const profile = await this.profileChangePositionRepository.findOne({
where: { id: v.id },
});
if (profile != null) {
// Add timeout
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 30000)
);
const apiCallPromise = new CallAPI().PostData(request, "/org/profile/salary", {
profileId: profile.id,
date: new Date(),
});
await Promise.race([apiCallPromise, timeoutPromise]);
profile.status = "DONE";
await this.profileChangePositionRepository.save(profile);
}
} catch (error) {
console.error(`Failed to process profile ${v.id}:`, error);
// Mark as FAILED แทนที่จะ leave as-is
const profile = await this.profileChangePositionRepository.findOne({
where: { id: v.id },
});
if (profile) {
profile.status = "FAILED";
profile.errorMessage = error.message;
await this.profileChangePositionRepository.save(profile);
}
throw error; // Re-throw to track in allSettled
}
}),
);
// Check results
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.error(`${failed.length} profiles failed to process`);
// Optionally return partial success info
}
```
---
### #4 - Database Operations Without Error Handling (HIGH)
**Files:** ทั้งหมด 9 Controllers
**Locations:** หลาย method ในทุกไฟล์
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- Database operations ส่วนใหญ่ไม่มี try-catch
- TypeORM query errors จะถูก catch โดย global error middleware แต่อาจเป็น generic 500 errors
- Connection timeout, database down, หรือ query errors จะไม่ได้รับการจัดการเฉพาะเจาะจง
- ไม่สามารถ distinguish ระหว่าง different error types ได้
**ตัวอย่าง Code ปัจจุบัน (เสี่ยง):**
```typescript
@Get("list")
public async listAuthRoleAttr() {
const getList = await this.authRoleAttrRepo.find();
// ❌ ถ้า database error จะ throw ไปยัง global middleware
// ไม่สามารถ handle เฉพาะเจาะจงได้
return new HttpSuccess(getList);
}
```
**Recommended Fix:**
สำหรับ critical operations:
```typescript
import { QueryFailedError } from "typeorm";
@Get("list")
public async listAuthRoleAttr() {
try {
const getList = await this.authRoleAttrRepo.find();
return new HttpSuccess(getList);
} catch (error) {
if (error instanceof QueryFailedError) {
// Handle database-specific errors
console.error('Database query failed:', error);
throw new HttpError(
HttpStatus.SERVICE_UNAVAILABLE,
"Database service temporarily unavailable"
);
} else if (error.message && error.message.includes('connection')) {
throw new HttpError(
HttpStatus.SERVICE_UNAVAILABLE,
"Unable to connect to database"
);
}
// Re-throw other errors to global middleware
throw error;
}
}
```
---
### #5 - Promise.all Without Error Handling (HIGH)
**File & Location:** [AuthRoleController.ts:247-267](src/controllers/AuthRoleController.ts#L247-L267)
**Method:** `editAuthRole`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- `Promise.all()` รวม `remove()` และหลาย `save()` operations
- ถ้า operation ไหน fail จะทำให้ **unhandled rejection**
- ไม่มี try-catch รองรับ
- Partial failure จะทำให้ไม่สามารถ recover ได้
**Code ปัจจุบัน (เสี่ยง):**
```typescript
await this.authRoleAttrRepo.remove(roleAttrData, { data: req });
const newAttrs = body.authRoleAttrs.map((attr) => {
const newAttr = new AuthRoleAttr();
Object.assign(newAttr, attr, {
authRoleId: roleId,
createdUserId: req.user.sub,
createdFullName: req.user.name,
lastUpdateUserId: req.user.sub,
lastUpdateFullName: req.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
});
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)), // ❌ ถ้า fail จะ unhandled rejection
]);
```
**Recommended Fix:**
```typescript
try {
await this.authRoleAttrRepo.remove(roleAttrData, { data: req });
const newAttrs = body.authRoleAttrs.map((attr) => {
const newAttr = new AuthRoleAttr();
Object.assign(newAttr, attr, {
authRoleId: roleId,
createdUserId: req.user.sub,
createdFullName: req.user.name,
lastUpdateUserId: req.user.sub,
lastUpdateFullName: req.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
});
return newAttr;
});
const before = structuredClone(record);
// ใช้ Promise.allSettled แทน Promise.all
const results = await Promise.allSettled([
this.authRoleRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)),
]);
// Check for failures
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
console.error('Some operations failed:', failures);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to update some role attributes"
);
}
// Redis flush with error handling (จากปัญหา #2)
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
try {
const redisFlushDbAsync = promisify(redisClient.flushdb).bind(redisClient);
await redisFlushDbAsync();
} catch (error) {
console.error('Redis flush failed:', error);
// Non-critical - don't fail the request
} finally {
if (redisClient) {
redisClient.quit();
}
}
return new HttpSuccess();
} catch (error) {
console.error('Failed to update role:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to update role"
);
}
```
---
### #6 - JWT Verification Inconsistent Error Handling (MEDIUM)
**File & Location:** [ApiKeyController.ts:42-61](src/controllers/ApiKeyController.ts#L42-L61)
**Method:** `verifyApiKey`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- มี try-catch แต่ return HttpSuccess แทนที่จะ throw error
- Error handling ไม่ consistent กับ endpoints อื่น
- Client จะไม่รู้ว่าเกิด error (เพราะได้ 200 OK พร้อม valid: false)
- ไม่สามารถ distinguish ระหว่าง token types ของ errors ได้
**Code ปัจจุบัน (เสี่ยง):**
```typescript
try {
const jwtSecret = process.env.JWT_SECRET || "your-default-secret-key";
const decoded = jwt.verify(requestBody.token, jwtSecret);
return new HttpSuccess({
valid: true,
data: decoded,
});
} catch (error: any) {
console.error("JWT Verification Error:", error.message);
return new HttpSuccess({ // ❌ Return success แม้ error
valid: false,
error: error.message,
});
}
```
**Recommended Fix:**
```typescript
try {
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"JWT secret not configured"
);
}
const decoded = jwt.verify(requestBody.token, jwtSecret);
return new HttpSuccess({
valid: true,
data: decoded,
});
} catch (error: any) {
console.error("JWT Verification Error:", error.message);
if (error.name === 'TokenExpiredError') {
throw new HttpError(HttpStatus.UNAUTHORIZED, "Token expired");
} else if (error.name === 'JsonWebTokenError') {
throw new HttpError(HttpStatus.UNAUTHORIZED, "Invalid token");
} else if (error instanceof HttpError) {
throw error;
}
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Token verification failed"
);
}
```
---
### #7 - Query Builder Without Error Handling (MEDIUM)
**File & Location:** [ChangePositionController.ts:284-350](src/controllers/ChangePositionController.ts#L284-L350)
**Method:** `GetProfileChangePositionLists`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- Complex QueryBuilder พร้อม Brackets และ dynamic conditions
- ถ้า query syntax error, database connection error, หรือ data type mismatch จะ throw ไป global middleware
- ไม่สามารถ log หรือ track specific query errors ได้
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const [profileChangePosition, total] = await AppDataSource.getRepository(ProfileChangePosition)
.createQueryBuilder("profileChangePosition")
.where({ changePositionId: changePositionId })
.andWhere(
new Brackets((qb) => {
qb.where(
searchKeyword != undefined && searchKeyword != null && searchKeyword != ""
? "profileChangePosition.prefix LIKE :keyword"
: "1=1",
{ keyword: `%${searchKeyword}%` },
)
// ... หลาย orWhere
}),
)
.orderBy("profileChangePosition.createdAt", "ASC")
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount(); // ❌ ไม่มี try-catch
return new HttpSuccess({ data: profileChangePosition, total });
```
**Recommended Fix:**
```typescript
try {
// Validate input
if (page < 1) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number");
}
if (pageSize < 1 || pageSize > 1000) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size");
}
const [profileChangePosition, total] = await AppDataSource.getRepository(ProfileChangePosition)
.createQueryBuilder("profileChangePosition")
.where({ changePositionId: changePositionId })
.andWhere(
new Brackets((qb) => {
// Use parameterized queries
const conditions = [];
const params = { keyword: `%${searchKeyword}%` };
if (searchKeyword) {
conditions.push("profileChangePosition.prefix LIKE :keyword");
conditions.push("profileChangePosition.firstName LIKE :keyword");
conditions.push("profileChangePosition.lastName LIKE :keyword");
conditions.push("profileChangePosition.citizenId LIKE :keyword");
conditions.push("profileChangePosition.birthDate LIKE :keyword");
conditions.push("profileChangePosition.lastUpdatedAt LIKE :keyword");
conditions.push("profileChangePosition.status LIKE :keyword");
}
qb.where(
searchKeyword ? conditions.join(" OR ") : "1=1",
params
);
}),
)
.orderBy("profileChangePosition.createdAt", "ASC")
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
return new HttpSuccess({ data: profileChangePosition, total });
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
console.error('Query failed:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to retrieve profile change positions"
);
}
```
---
### #8 - Null Reference Risk (MEDIUM)
**File & Location:** [ApiWebServiceController.ts:67-78](src/controllers/ApiWebServiceController.ts#L67-L78)
**Method:** `listAttribute`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- `revision` อาจเป็น null ถ้าไม่พบ record
- การใช้ `revision?.id` จะทำให้ condition เป็น `PosMaster.orgRevisionId = "undefined"`
- SQL query จะไม่ error แต่จะ return ผลลัพธ์ที่ไม่ถูกต้อง
- ไม่มี validation ว่า revision ต้องมีค่า
**Code ปัจจุบัน (เสี่ยง):**
```typescript
} else if (system == "organization") {
tbMain = "OrgRoot";
const revision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
condition = `OrgRoot.orgRevisionId = "${revision?.id}"`; // ❌ ถ้า revision เป็น null จะเป็น undefined
}
```
**Recommended Fix:**
```typescript
} else if (system == "organization") {
tbMain = "OrgRoot";
const revision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
if (!revision) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"No current organization revision found"
);
}
condition = `OrgRoot.orgRevisionId = "${revision.id}"`;
}
```
---
### #9 - Unsafe Default Environment Variable (LOW)
**File & Location:** [ApiKeyController.ts:45](src/controllers/ApiKeyController.ts#L45)
**Method:** `verifyApiKey`
**Problem Type:** 2. Missing Error Handle / Security
**Root Cause:**
- ใช้ default value สำหรับ JWT_SECRET
- ใน production ถ้าไม่ได้ set JWT_SECRET จะใช้ default value ที่ไม่ปลอดภัย
- อาจนำไปสู่ security breach
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const jwtSecret = process.env.JWT_SECRET || "your-default-secret-key"; // ❌ Default value insecure
```
**Recommended Fix:**
```typescript
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
if (process.env.NODE_ENV === 'production') {
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"JWT secret not configured"
);
}
// Only for development
console.warn('Using default JWT secret - not safe for production!');
}
const decoded = jwt.verify(requestBody.token, jwtSecret || 'dev-secret-key');
```
---
### #10 - Switch Statement Without Break (BUG)
**File & Location:** [ChangePositionController.ts:430-515](src/controllers/ChangePositionController.ts#L430-L515)
**Method:** `positionProfileEmployee`
**Problem Type:** 3. Logic Bug (ส่งผลต่อ data consistency)
**Root Cause:**
- Switch statement ไม่มี `break` statements
- จะเกิด **fallthrough** effect - ทุก case หลังจาก case ที่ match จะถูก execute ด้วย
- จะทำให้ data ถูก overwrite ด้วยค่าจาก cases ถัดไป
- เป็น common bug ที่อาจทำให้ data corruption
**Code ปัจจุบัน (เสี่ยง):**
```typescript
switch (body.node) {
case 0: {
const data = await this.orgRootRepository.findOne({
where: { id: body.nodeId },
});
if (data != null) {
profileChangePos.rootId = data.id;
profileChangePos.root = data.orgRootName;
profileChangePos.rootShortName = data.orgRootShortName;
}
} // ❌ ไม่มี break
case 1: { // ❌ จะ execute ถ้า case 0 match
const data = await this.child1Repository.findOne({
where: { id: body.nodeId },
relations: ["orgRoot"],
});
// ...
} // ❌ ไม่มี break
case 2: { // ❌ จะ execute ถ้า case 0 หรือ 1 match
// ...
}
// ... ต่อไปเรื่อยๆ
}
```
**Recommended Fix:**
```typescript
switch (body.node) {
case 0: {
const data = await this.orgRootRepository.findOne({
where: { id: body.nodeId },
});
if (data != null) {
profileChangePos.rootId = data.id;
profileChangePos.root = data.orgRootName;
profileChangePos.rootShortName = data.orgRootShortName;
}
break; // ✅ เพิ่ม break
}
case 1: {
const data = await this.child1Repository.findOne({
where: { id: body.nodeId },
relations: ["orgRoot"],
});
if (data != null) {
profileChangePos.rootId = data.orgRoot.id;
profileChangePos.root = data.orgRoot.orgRootName;
profileChangePos.rootShortName = data.orgRoot.orgRootShortName;
profileChangePos.child1Id = data.id;
profileChangePos.child1 = data.orgChild1Name;
profileChangePos.child1ShortName = data.orgChild1ShortName;
}
break; // ✅ เพิ่ม break
}
case 2: {
const data = await this.child2Repository.findOne({
where: { id: body.nodeId },
relations: ["orgRoot", "orgChild1"],
});
if (data != null) {
profileChangePos.rootId = data.orgRoot.id;
profileChangePos.root = data.orgRoot.orgRootName;
profileChangePos.rootShortName = data.orgRoot.orgRootShortName;
profileChangePos.child1Id = data.orgChild1.id;
profileChangePos.child1 = data.orgChild1.orgChild1Name;
profileChangePos.child1ShortName = data.orgChild1.orgChild1ShortName;
profileChangePos.child2Id = data.id;
profileChangePos.child2 = data.orgChild2Name;
profileChangePos.child2ShortName = data.orgChild2ShortName;
}
break; // ✅ เพิ่ม break
}
case 3: {
// ... เพิ่ม break ท้าย
}
case 4: {
// ... เพิ่ม break ท้าย
}
}
```
---
### #11 - Array Mutation in Loop (MEDIUM)
**File & Location:** [ChangePositionController.ts:233-250](src/controllers/ChangePositionController.ts#L233-L250)
**Method:** `CreateProfileChangePosition`
**Problem Type:** 3. Logic Bug
**Root Cause:**
- ใช้ตัวแปร `profiles` เดียวแล้ว push เข้า array หลายครั้ง
- ทุก elements ใน array จะชี้ไปที่ object เดียวกัน
- ทำให้ข้อมูลซ้ำกันทั้งหมด
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const profileChangePositions: ProfileChangePosition[] = [];
const profiles = new ProfileChangePosition(); // ❌ สร้างครั้งเดียว
for (const data of body.profiles) {
Object.assign(profiles, data); // ❌ ใช้ object เดียว
// ...
profileChangePositions.push(profiles); // ❌ push object เดียวกันซ้ำๆ
}
await this.profileChangePositionRepository.save(profileChangePositions);
```
**Recommended Fix:**
```typescript
const profileChangePositions: ProfileChangePosition[] = [];
for (const data of body.profiles) {
const profiles = new ProfileChangePosition(); // ✅ สร้างใหม่ทุกรอบ
Object.assign(profiles, data);
let positionOld = data.positionOld ? `${data.positionOld}` : "";
let rootOld = data.rootOld ? (data.positionOld ? `/${data.rootOld}` : `${data.rootOld}`) : "";
profiles.changePositionId = changePositionId;
profiles.organizationPositionOld = `${positionOld}${rootOld}`;
profiles.status = "WAITTING";
profiles.createdUserId = request.user.sub;
profiles.createdFullName = request.user.name;
profiles.createdAt = new Date();
profiles.lastUpdateUserId = request.user.sub;
profiles.lastUpdateFullName = request.user.name;
profiles.lastUpdatedAt = new Date();
profileChangePositions.push(profiles);
}
await this.profileChangePositionRepository.save(profileChangePositions);
```
---
## สรุปคำแนะนำการแก้ไขแบบรวม
### ระดับความสำคัญ
**ต้องแก้ทันที (P0 - Critical):**
1. Redis operations error handling - อาจทำให้ process crash
2. External API calls error handling - อาจทำให้ unhandled rejection
**ควรแก้โดยเร็ว (P1 - High):**
3. Database operations error handling
4. Promise operations error handling
**ควรแก้ (P2 - Medium):**
5. JWT verification consistency
6. Query builder error handling
7. Null reference checks
**แก้เมื่อว่าง (P3 - Low):**
8. Environment variable defaults
9. Code quality issues
### แนวทางการแก้ไขแบบ Global
1. **Implement centralized error handling:**
- Wrap all async operations
- Use specific error types
- Log all errors appropriately
2. **Add circuit breaker for external services:**
- Redis, external APIs
- Prevent cascade failures
3. **Use Promise.allSettled** แทน Promise.all สำหรับ independent operations
4. **Add input validation:**
- Validate before processing
- Check for null/undefined
5. **Implement retry logic:**
- For transient failures
- Database connection issues
---
## ไฟล์ที่ต้องแก้ไข
1. **src/controllers/AuthRoleController.ts** - Redis operations, Promise operations
2. **src/controllers/ChangePositionController.ts** - External API calls, Switch bug, Array mutation
3. **src/controllers/ApiKeyController.ts** - JWT verification, Environment variables
4. **src/controllers/ApiWebServiceController.ts** - Null reference checks
---
## ข้อมูลเพิ่มเติม
- **Controllers ที่ยังไม่ได้ตรวจสอบ:** 130 ไฟล์
- **ไฟล์ที่ไม่สามารถอ่านได้:** CommandController.ts (ไฟล์ใหญ่เกิน 336KB)
---
**รายงานนี้ถูกสร้างโดย AI Code Review System**
**สำหรับ BMA EHR Organization Project**

View file

@ -1,829 +0,0 @@
# รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 2 (ไฟล์ที่ 11-20)
**Project:** BMA EHR Organization Backend
**Framework:** TSOA + Express + TypeORM
**วันที่ตรวจสอบ:** 2026-05-08
**จำนวน Controllers:** 10 ไฟล์
**สถานะ:** เสร็จสิ้น
---
## สรุปผลการตรวจสอบ
| ระดับความรุนแรง | จำนวนจุดเสี่ยง |
|---------------------|-------------------|
| **CRITICAL** | 1 |
| **HIGH** | 3 |
| **MEDIUM** | 4 |
| **LOW** | 2 |
| **BUG** | 2 |
| **รวมทั้งหมด** | 12 |
---
## Controllers ที่ตรวจสอบ
11. [CommandOperatorController.ts](src/controllers/CommandOperatorController.ts)
12. [CommandSalaryController.ts](src/controllers/CommandSalaryController.ts)
13. [CommandSysController.ts](src/controllers/CommandSysController.ts)
14. [CommandTypeController.ts](src/controllers/CommandTypeController.ts)
15. [DPISController.ts](src/controllers/DPISController.ts)
16. [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts)
17. [DistrictController.ts](src/controllers/DistrictController.ts)
18. [EducationLevelController.ts](src/controllers/EducationLevelController.ts)
19. [EmployeePosLevelController.ts](src/controllers/EmployeePosLevelController.ts)
20. [EmployeePosTypeController.ts](src/controllers/EmployeePosTypeController.ts)
---
## รายละเอียดจุดเสี่ยงแต่ละจุด
### #1 - Transaction QueryRunner Not Released on Error (CRITICAL)
**File & Location:** [CommandOperatorController.ts:169-222](src/controllers/CommandOperatorController.ts#L169-L222)
**Method:** `deleteCommandOperator`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- ใช้ QueryRunner และ Transaction แต่มี error handling ที่ไม่ปลอดภัย
- ถ้าเกิด error หลังจาก `throw error` ใน catch block แล้ว จะไม่ถึง `finally` block
- QueryRunner จะไม่ถูก release ทำให้ connection leak
- ในกรณีที่ HttpError ถูก throw ภายใน catch จะไม่มีการ release queryRunner
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// ... operations
await queryRunner.commitTransaction();
return new HttpSuccess(true);
} catch (error) {
await queryRunner.rollbackTransaction();
throw error; // ❌ ถ้า throw HttpError จะไม่ถึง finally
} finally {
await queryRunner.release(); // ❌ จะไม่ถูกเรียกถ้า throw error ใน catch
}
```
**Recommended Fix:**
```typescript
const queryRunner = AppDataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. หา operator
const operator = await queryRunner.manager.findOne(CommandOperator, {
where: {
id: operatorId,
commandId: commandId,
},
});
if (!operator) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบเจ้าหน้าที่ดำเนินการ");
}
const removedOrderNo = operator.orderNo;
// 3. ลบ
await queryRunner.manager.remove(operator);
// 4. re orderNumber ตัวที่เหลือ
await queryRunner.manager
.createQueryBuilder()
.update(CommandOperator)
.set({
orderNo: () => "orderNo - 1",
})
.where("commandId = :commandId", { commandId })
.andWhere("orderNo > :removedOrderNo", { removedOrderNo })
.execute();
await queryRunner.commitTransaction();
return new HttpSuccess(true);
} catch (error) {
await queryRunner.rollbackTransaction();
// Re-throw after rollback
throw error;
}
} finally {
// ✅ ใช้ finally block ระดับนอกสุดเพื่อให้แน่ใจว่าจะถูกเรียกเสมอ
if (queryRunner.isReleased) {
// Already released, skip
} else {
await queryRunner.release();
}
}
```
---
### #2 - Promise.all Without Error Handling in Development Request (HIGH)
**File & Location:** [DevelopmentRequestController.ts:349-364](src/controllers/DevelopmentRequestController.ts#L349-L364)
**Method:** `newDevelopmentRequest`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- `Promise.all()` กับการบันทึก development projects หลายรายการ
- ถ้ามี project ไหน save ไม่สำเร็จ จะทำให้ unhandled rejection
- ไม่มี try-catch รองรับ
- External API call ใช้ `.catch()` แต่ไม่ได้ throw error ต่อ
**Code ปัจจุบัน (เสี่ยง):**
```typescript
if (body.developmentProjects != null) {
await Promise.all(
body.developmentProjects.map(async (x) => {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = data.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before, after: developmentProject });
}),
);
}
await new CallAPI()
.PostData(req, "/org/workflow/add-workflow", {
refId: data.id,
sysName: "REGISTRY_IDP",
posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false,
orgRootId: orgRoot?.id ?? null
})
.catch((error) => {
console.error("Error calling API:", error);
});
```
**Recommended Fix:**
```typescript
if (body.developmentProjects != null) {
try {
await Promise.all(
body.developmentProjects.map(async (x) => {
try {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = data.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before, after: developmentProject });
} catch (error) {
console.error(`Failed to save development project "${x}":`, error);
throw error; // Re-throw to be caught by Promise.all
}
}),
);
} catch (error) {
console.error("Failed to save some development projects:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to save development projects"
);
}
}
// Call external API with proper error handling
try {
await new CallAPI().PostData(req, "/org/workflow/add-workflow", {
refId: data.id,
sysName: "REGISTRY_IDP",
posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false,
orgRootId: orgRoot?.id ?? null
});
} catch (error) {
console.error("Failed to call workflow API:", error);
// Optionally mark the request as having workflow issues
// But don't fail the entire request
}
```
---
### #3 - QueryBuilder with Dynamic Conditions Without Error Handling (HIGH)
**File & Location:** [DevelopmentRequestController.ts:122-265](src/controllers/DevelopmentRequestController.ts#L122-L265)
**Method:** `getDevelopmentRequestAdmin`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- Complex QueryBuilder พร้อม dynamic conditions หลายอย่าง
- ไม่มี try-catch รองรับ
- Permission check อาจ throw error
- Null reference risks หลายจุด (`orgRevisionPublish?.id`, `data.root`, etc.)
**Recommended Fix:**
```typescript
@Get("admin")
public async getDevelopmentRequestAdmin(
@Request() request: RequestWithUser,
@Query("status") status: string,
@Query("keyword") keyword: string = "",
@Query("page") page: number = 1,
@Query("pageSize") pageSize: number = 10,
@Query("sortBy") sortBy?: string,
@Query("descending") descending?: boolean,
) {
try {
// Validate inputs
if (page < 1) throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number");
if (pageSize < 1 || pageSize > 1000) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size");
}
let data = await new permission().PermissionOrgList(request, "SYS_REGISTRY_OFFICER");
const orgRevisionPublish = await this.orgRevisionRepository
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true")
.getOne();
let query = await AppDataSource.getRepository(DevelopmentRequest)
.createQueryBuilder("developmentRequest")
.leftJoinAndSelect("developmentRequest.profile", "profile")
.leftJoinAndSelect("profile.current_holders", "current_holders")
.leftJoinAndSelect("current_holders.orgRevision", "orgRevision")
.andWhere(
status == undefined || status.trim().toUpperCase() == "ALL" || status == ""
? "1=1"
: "developmentRequest.status = :status",
{
status: status == undefined || status == null ? "" : status.trim().toUpperCase(),
},
)
.andWhere(
orgRevisionPublish ? `current_holders.orgRevisionId = :revisionId` : "1=1",
{
revisionId: orgRevisionPublish?.id,
},
)
// ... rest of the query
const [lists, total] = await query
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
const _data = lists.map((item) => ({ ...item, profile: null }));
return new HttpSuccess({ data: _data, total });
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
console.error('Failed to get development requests:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to retrieve development requests"
);
}
}
```
---
### #4 - Promise.all in Edit Development Request Without Error Handling (HIGH)
**File & Location:** [DevelopmentRequestController.ts:402-417](src/controllers/DevelopmentRequestController.ts#L402-L417)
**Method:** `editUserDevelopmentRequest`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- ใช้ `Promise.all()` โดยไม่มี error handling
- Similar to #2 but in edit operation
**Code ปัจจุบัน (เสี่ยง):**
```typescript
await this.developmentProjectRepository.delete({ developmentRequestId: record.id });
if (body.developmentProjects != null) {
await Promise.all(
body.developmentProjects.map(async (x) => {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = record.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before: null, after: record });
}),
);
}
```
**Recommended Fix:**
```typescript
await this.developmentProjectRepository.delete({ developmentRequestId: record.id });
if (body.developmentProjects != null) {
try {
await Promise.all(
body.developmentProjects.map(async (x) => {
try {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = record.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before: null, after: developmentProject });
} catch (error) {
console.error(`Failed to update development project "${x}":`, error);
throw error;
}
}),
);
} catch (error) {
console.error("Failed to update development projects:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to update development projects"
);
}
}
```
---
### #5 - Null Reference Risk in Profile Query (MEDIUM)
**File & Location:** [DPISController.ts:272-275](src/controllers/DPISController.ts#L272-L275)
**Method:** `GetProfileCitizenIdAsync`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- `findRevision` อาจเป็น null ถ้าไม่พบ current revision
- การใช้ `findRevision?.id` ใน `find()` operation จะเป็น undefined
- `current_holders?.find()` อาจ return undefined
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const findRevision = await this.orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true },
});
var current_holder = profile.current_holders?.find((x) => x.orgRevisionId == findRevision?.id);
const mapProfile: DPISResult = {
// ...
organization: {
orgRootName: current_holder?.orgRoot?.orgRootName || "", // ❌ multiple levels of null checks
orgChild1Name: current_holder?.orgChild1?.orgChild1Name || "",
// ...
},
};
```
**Recommended Fix:**
```typescript
const findRevision = await this.orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true },
});
if (!findRevision) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"No current organization revision found"
);
}
var current_holder = profile.current_holders?.find((x) => x.orgRevisionId == findRevision.id);
if (!current_holder) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"No current organization assignment found for this profile"
);
}
const mapProfile: DPISResult = {
// ...
organization: {
orgRootName: current_holder.orgRoot?.orgRootName || "",
orgChild1Name: current_holder.orgChild1?.orgChild1Name || "",
orgChild2Name: current_holder.orgChild2?.orgChild2Name || "",
orgChild3Name: current_holder.orgChild3?.orgChild3Name || "",
orgChild4Name: current_holder.orgChild4?.orgChild4Name || "",
},
};
```
---
### #6 - Unsafe Optional Chain in OrgRoot Query (MEDIUM)
**File & Location:** [DevelopmentRequestController.ts:322-330](src/controllers/DevelopmentRequestController.ts#L322-L330)
**Method:** `newDevelopmentRequest`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- ใช้ optional chaining (`?.`) และ nullish coalescing ใน query
- `find()` อาจ return undefined และการใช้ `!` (non-null assertion) อาจทำให้ runtime error
**Code ปัจจุบัน (เสี่ยง):**
```typescript
const orgRoot = await this.orgRootRepo.findOne({
select: {
id: true,
isDeputy: true
},
where: {
id: profile.current_holders.find(x => x.orgRootId)!.orgRootId ?? "" // ❌ unsafe
}
})
```
**Recommended Fix:**
```typescript
const currentHolder = profile.current_holders.find(x => x.orgRootId);
if (!currentHolder || !currentHolder.orgRootId) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Profile must have a current organization assignment"
);
}
const orgRoot = await this.orgRootRepo.findOne({
select: {
id: true,
isDeputy: true
},
where: {
id: currentHolder.orgRootId
}
});
if (!orgRoot) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Organization root not found"
);
}
```
---
### #7 - Promise.all in Admin Edit Without Error Handling (MEDIUM)
**File & Location:** [DevelopmentRequestController.ts:467-490](src/controllers/DevelopmentRequestController.ts#L467-L490)
**Method:** `editAdminDevelopmentRequest`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- `Promise.all()` กับ nested save operations
- ไม่มี error handling สำหรับ individual promises
**Recommended Fix:**
```typescript
if (record.developmentProjects != null) {
try {
await Promise.all(
record.developmentProjects.map(async (x) => {
try {
let developmentProject = new DevelopmentProject();
let developmentProjectHistory = new DevelopmentProject();
Object.assign(developmentProject, {
...meta,
id: undefined,
name: record.name,
profileDevelopmentId: profileDevelopment.id,
});
Object.assign(developmentProject, {
...meta,
id: undefined,
name: record.name,
profileDevelopmentHistoryId: history.id,
});
await Promise.all([
this.developmentProjectRepository.save(developmentProject, { data: req }),
setLogDataDiff(req, { before: null, after: developmentProject }),
this.developmentProjectRepository.save(developmentProjectHistory, { data: req }),
]);
} catch (error) {
console.error(`Failed to save development project for "${record.name}":`, error);
throw error;
}
}),
);
} catch (error) {
console.error("Failed to save development projects:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to save development projects"
);
}
}
```
---
### #8 - QueryBuilder Parameters Without Validation (MEDIUM)
**File & Location:** [CommandSalaryController.ts:73-108](src/controllers/CommandSalaryController.ts#L73-L108)
**Method:** `GetAdmin`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- QueryBuilder พร้อม dynamic conditions
- ไม่มี input validation
- Page number validation เป็น optional (มี default value แต่ไม่ validate range)
**Recommended Fix:**
```typescript
@Get("admin")
async GetAdmin(
@Query("page") page: number = 1,
@Query("pageSize") pageSize: number = 10,
@Query() commandSysId?: string | null,
@Query() isActive?: boolean | null,
@Query() searchKeyword?: string | null,
) {
try {
// Validate inputs
if (page < 1 || page > 10000) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number");
}
if (pageSize < 1 || pageSize > 1000) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size");
}
const [commandSalarys, total] = await this.commandSalaryRepository
.createQueryBuilder("commandSalary")
.andWhere(
isActive != null && isActive != undefined ? "commandSalary.isActive = :isActive" : "1=1",
{
isActive:
isActive == null || isActive == undefined ? null : `${isActive == true ? 1 : 0}`,
},
)
// ... rest of query
.getManyAndCount();
return new HttpSuccess({ commandSalarys, total });
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
console.error('Failed to get command salaries:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to retrieve command salaries"
);
}
}
```
---
### #9 - Hardcoded Response Data (LOW)
**File & Location:** [CommandTypeController.ts:140-199](src/controllers/CommandTypeController.ts#L140-L199)
**Method:** `GetById`
**Problem Type:** 3. Code Quality
**Root Cause:**
- Hardcoded template data ใน code
- ไม่ flexible และยากต่อการ maintain
- ควรเก็บใน database หรือ configuration
**Code ปัจจุบัน (เสี่ยง):**
```typescript
if (_commandType.code == "C-PM-10") {
let _commandType10: any;
_commandType10 = {
// ... hardcoded fields
name1: "๑. ..........................ประธาน",
name2: "๒. ..........................กรรมการ",
// ...
};
_commandType = _commandType10;
} else if (["C-PM-21", "C-PM-23"].includes(_commandType.code)) {
let _commandType21and23: any;
_commandType21and23 = {
// ... hardcoded fields
persons: [
{
no: "",
org: "",
// ...
},
],
};
_commandType = _commandType21and23;
}
```
**Recommended Fix:**
```typescript
// Move these templates to database or configuration
const commandTemplates = await this.commandTemplateRepository.find({
where: { commandTypeCode: _commandType.code }
});
if (commandTemplates.length > 0) {
const template = commandTemplates[0];
return new HttpSuccess({
..._commandType,
...template.templateData
});
}
return new HttpSuccess(_commandType);
```
---
### #10 - Missing Transaction for Related Operations (LOW)
**File & Location:** [CommandOperatorController.ts:109-112](src/controllers/CommandOperatorController.ts#L109-L112)
**Method:** `swapCommandOperator`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- มีการ swap orderNo ระหว่าง 2 records
- ไม่ได้ใช้ transaction
- ถ้า save ตัวแรกสำเร็จ แต่ตัวที่สอง fail จะเกิด data inconsistency
**Code ปัจจุบัน (เสี่ยง):**
```typescript
// swap
const temp = source.orderNo;
source.orderNo = dest.orderNo;
dest.orderNo = temp;
await Promise.all([
this.commandOperatorRepo.save(source),
this.commandOperatorRepo.save(dest),
]);
```
**Recommended Fix:**
```typescript
const queryRunner = AppDataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
// swap
const temp = source.orderNo;
source.orderNo = dest.orderNo;
dest.orderNo = temp;
await Promise.all([
queryRunner.manager.save(CommandOperator, source),
queryRunner.manager.save(CommandOperator, dest),
]);
await queryRunner.commitTransaction();
return new HttpSuccess();
} catch (error) {
await queryRunner.rollbackTransaction();
console.error('Failed to swap command operators:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to swap operator order"
);
} finally {
await queryRunner.release();
}
```
---
### #11 - Wrong Error Status Code (BUG)
**File & Location:** [Multiple Files - CommandSysController.ts:127](src/controllers/CommandSysController.ts#L127), [CommandTypeController.ts:216](src/controllers/CommandTypeController.ts#L216), etc.
**Methods:** `Post` in various controllers
**Problem Type:** 3. Logic Bug
**Root Cause:**
- Throw `HttpError(HttpStatusCode.NOT_FOUND, ...)` สำหรับ duplicate name errors
- ควรใช้ `BAD_REQUEST` หรือ `CONFLICT` แทน
**Code ปัจจุบัน (ผิด):**
```typescript
if (checkName) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ชื่อนี้มีอยู่ในระบบแล้ว"); // ❌ Wrong status code
}
```
**Recommended Fix:**
```typescript
if (checkName) {
throw new HttpError(HttpStatusCode.CONFLICT, "ชื่อนี้มีอยู่ในระบบแล้ว"); // ✅ Correct status code
}
```
---
### #12 - Typos in Status Field (BUG)
**File & Location:** [ChangePositionController.ts:79](src/controllers/ChangePositionController.ts#L79)
**Method:** `CreateChangePosition`
**Problem Type:** 3. Logic Bug
**Root Cause:**
- Status เป็น "WAITTING" (ตัว T เกิน)
- ควรเป็น "WAITING"
**Code ปัจจุบัน (ผิด):**
```typescript
changePosition.status = "WAITTING"; // ❌ typo
```
**Recommended Fix:**
```typescript
changePosition.status = "WAITING"; // ✅ correct spelling
```
**หมายเหตุ:** ปัญหาเดียวกันนี้พบใน [ChangePositionController.ts:241](src/controllers/ChangePositionController.ts#L241)
---
## สรุปคำแนะนำการแก้ไขแบบรวม
### ระดับความสำคัญ
**ต้องแก้ทันที (P0 - Critical):**
1. QueryRunner transaction not released on error - อาจทำให้ connection leak
**ควรแก้โดยเร็ว (P1 - High):**
2. Promise.all operations without error handling - unhandled rejections
3. QueryBuilder without validation and error handling
**ควรแก้ (P2 - Medium):**
4. Null reference checks
5. Unsafe optional chain usage
6. Promise operations in edit methods
**แก้เมื่อว่าง (P3 - Low):**
7. Hardcoded data
8. Missing transaction for related operations
### แนวทางการแก้ไขแบบ Global
1. **Use try-finally pattern** for all QueryRunner operations
2. **Add input validation** for all query parameters
3. **Use Promise.allSettled** หรือ wrap promises in try-catch
4. **Implement proper null checks** ก่อน accessing nested properties
5. **Use transactions** สำหรับ operations ที่ต้องการ consistency
---
## ไฟล์ที่ต้องแก้ไข
1. **src/controllers/CommandOperatorController.ts** - Transaction handling, Promise operations
2. **src/controllers/DevelopmentRequestController.ts** - Promise.all, QueryBuilder validation
3. **src/controllers/DPISController.ts** - Null reference checks
4. **src/controllers/CommandSalaryController.ts** - Input validation
5. **src/controllers/CommandTypeController.ts** - Error status codes, hardcoded data
---
## ข้อมูลเพิ่มเติม
- **Controllers ที่ยังไม่ได้ตรวจสอบ:** 120 ไฟล์
- **จุดเสี่ยงที่พบซ้ำจากชุดที่ 1:** Promise.all without error handling, QueryBuilder without error handling
---
**รายงานนี้ถูกสร้างโดย AI Code Review System**
**สำหรับ BMA EHR Organization Project**

View file

@ -1,874 +0,0 @@
# รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 3 (ไฟล์ที่ 21-30)
**Project:** BMA EHR Organization Backend
**Framework:** TSOA + Express + TypeORM
**วันที่ตรวจสอบ:** 2026-05-08
**จำนวน Controllers:** 10 ไฟล์
**สถานะ:** เสร็จสิ้น
---
## สรุปผลการตรวจสอบ
| ระดับความรุนแรง | จำนวนจุดเสี่ยง |
|---------------------|-------------------|
| **CRITICAL** | 0 |
| **HIGH** | 4 |
| **MEDIUM** | 5 |
| **LOW** | 2 |
| **BUG** | 2 |
| **รวมทั้งหมด** | 13 |
---
## Controllers ที่ตรวจสอบ
21. [EmployeePositionController.ts](src/controllers/EmployeePositionController.ts)
22. [EmployeeTempPositionController.ts](src/controllers/EmployeeTempPositionController.ts)
23. [ExRetirementController.ts](src/controllers/ExRetirementController.ts)
24. [GenderController.ts](src/controllers/GenderController.ts)
25. [ImportDataController.ts](src/controllers/ImportDataController.ts)
26. [InsigniaController.ts](src/controllers/InsigniaController.ts)
27. [InsigniaTypeController.ts](src/controllers/InsigniaTypeController.ts)
28. [IssuesController.ts](src/controllers/IssuesController.ts)
29. [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts)
30. [LoginController.ts](src/controllers/LoginController.ts)
---
## รายละเอียดจุดเสี่ยงแต่ละจุด
### #1 - Promise.all Without Error Handling in Position Creation (HIGH)
**File & Location:** [EmployeePositionController.ts:690-707](src/controllers/EmployeePositionController.ts#L690-L707)
**Method:** `createEmpMaster`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- ใช้ `Promise.all()` สำหรับบันทึก positions หลายรายการพร้อมกัน
- ถ้ามี position ไหน save ไม่สำเร็จ จะเกิด unhandled rejection
- ไม่มี try-catch รองรับ ทำให้ไม่สามารถควบคุม error ได้
- ส่งผลให้อาจเกิด data inconsistency ถ้า save บางส่วนสำเร็จ แต่บางส่วนล้มเหลว
**Code ปัจจุบัน (เสี่ยง):**
```typescript
await Promise.all(
requestBody.positions.map(async (x: any) => {
const position = Object.assign(new EmployeePosition());
position.positionName = x.posDictName;
position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
position.positionIsSelected = false;
position.posMasterId = posMaster.id;
position.createdUserId = request.user.sub;
position.createdFullName = request.user.name;
position.createdAt = new Date();
position.lastUpdateUserId = request.user.sub;
position.lastUpdateFullName = request.user.name;
position.lastUpdatedAt = new Date();
await this.employeePositionRepository.save(position, { data: request });
}),
);
return new HttpSuccess(posMaster.id);
```
**Recommended Fix:**
```typescript
try {
await Promise.all(
requestBody.positions.map(async (x: any) => {
try {
const position = Object.assign(new EmployeePosition());
position.positionName = x.posDictName;
position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
position.positionIsSelected = false;
position.posMasterId = posMaster.id;
position.createdUserId = request.user.sub;
position.createdFullName = request.user.name;
position.createdAt = new Date();
position.lastUpdateUserId = request.user.sub;
position.lastUpdateFullName = request.user.name;
position.lastUpdatedAt = new Date();
await this.employeePositionRepository.save(position, { data: request });
} catch (error) {
console.error(`Failed to save position "${x.posDictName}":`, error);
throw error; // Re-throw to be caught by Promise.all
}
}),
);
return new HttpSuccess(posMaster.id);
} catch (error) {
console.error("Failed to save positions:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to save positions"
);
}
```
---
### #2 - Promise.all Without Error Handling in Position Update (HIGH)
**File & Location:** [EmployeePositionController.ts:905-921](src/controllers/EmployeePositionController.ts#L905-L921)
**Method:** `updateEmpMaster`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- Similar to #1 แต่เกิดใน update operation
- `Promise.all()` โดยไม่มี error handling
- เกิดการลบ positions เก่า ก่อน แล้วค่อยสร้างใหม่ ถ้าสร้างใหม่ fail จะเกิด data loss
**Code ปัจจุบัน (เสี่ยง):**
```typescript
await this.employeePositionRepository.delete({ posMasterId: posMaster.id });
await Promise.all(
requestBody.positions.map(async (x: any) => {
const position = Object.assign(new EmployeePosition());
position.positionName = x.posDictName;
position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
position.positionIsSelected = false;
position.posMasterId = posMaster.id;
position.createdUserId = request.user.sub;
position.createdFullName = request.user.name;
position.createdAt = new Date();
position.lastUpdateUserId = request.user.sub;
position.lastUpdateFullName = request.user.name;
position.lastUpdatedAt = new Date();
await this.employeePositionRepository.save(position, { data: request });
}),
);
```
**Recommended Fix:**
```typescript
// Get existing positions as backup before deletion
const existingPositions = await this.employeePositionRepository.find({
where: { posMasterId: posMaster.id }
});
try {
await this.employeePositionRepository.delete({ posMasterId: posMaster.id });
await Promise.all(
requestBody.positions.map(async (x: any) => {
try {
const position = Object.assign(new EmployeePosition());
position.positionName = x.posDictName;
position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
position.positionIsSelected = false;
position.posMasterId = posMaster.id;
position.createdUserId = request.user.sub;
position.createdFullName = request.user.name;
position.createdAt = new Date();
position.lastUpdateUserId = request.user.sub;
position.lastUpdateFullName = request.user.name;
position.lastUpdatedAt = new Date();
await this.employeePositionRepository.save(position, { data: request });
} catch (error) {
console.error(`Failed to update position "${x.posDictName}":`, error);
throw error;
}
}),
);
} catch (error) {
console.error("Failed to update positions, restoring backup:", error);
// Restore backup positions
await this.employeePositionRepository.save(existingPositions);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to update positions"
);
}
```
---
### #3 - Async forEach Without Proper Error Handling (HIGH)
**File & Location:** [EmployeePositionController.ts:2378-2395](src/controllers/EmployeePositionController.ts#L2378-L2395)
**Method:** `createEmpHolder`
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
- ใช้ `forEach` กับ async function ซึ่งไม่รอให้ทุก operation สำเร็จ
- การใช้ `forEach` กับ async จะไม่ catch error ที่เกิดใน loop
- ถ้ามีการ save ที่ fail จะไม่ทราบ และ data อาจไม่ถูกต้อง
**Code ปัจจุบัน (เสี่ยง):**
```typescript
dataMaster.positions.forEach(async (position) => {
if (position.id === requestBody.position) {
position.positionIsSelected = true;
const profile = await this.profileRepository.findOne({
where: { id: requestBody.profileId },
});
if (profile != null) {
const _null: any = null;
profile.posLevelId = position?.posLevelId ?? _null;
profile.posTypeId = position?.posTypeId ?? _null;
profile.position = position?.positionName ?? _null;
await this.profileRepository.save(profile);
}
} else {
position.positionIsSelected = false;
}
await this.employeePositionRepository.save(position);
});
```
**Recommended Fix:**
```typescript
// Use Promise.all instead of forEach with async
await Promise.all(
dataMaster.positions.map(async (position) => {
try {
if (position.id === requestBody.position) {
position.positionIsSelected = true;
const profile = await this.profileRepository.findOne({
where: { id: requestBody.profileId },
});
if (profile != null) {
const _null: any = null;
profile.posLevelId = position?.posLevelId ?? _null;
profile.posTypeId = position?.posTypeId ?? _null;
profile.position = position?.positionName ?? _null;
await this.profileRepository.save(profile);
}
} else {
position.positionIsSelected = false;
}
await this.employeePositionRepository.save(position);
} catch (error) {
console.error(`Failed to update position ${position.id}:`, error);
throw error;
}
})
);
```
---
### #4 - Promise.all in EmployeeTempPositionController Without Error Handling (HIGH)
**File & Location:** [EmployeeTempPositionController.ts:557-574](src/controllers/EmployeeTempPositionController.ts#L557-L574)
**Method:** `createEmpMaster`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- เหมือนกับ #1 แต่เกิดใน EmployeeTempPositionController
- ใช้ `Promise.all()` โดยไม่มี error handling
**Code ปัจจุบัน (เสี่ยง):**
```typescript
await Promise.all(
requestBody.positions.map(async (x: any) => {
const position = Object.assign(new EmployeePosition());
position.positionName = x.posDictName;
position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
position.positionIsSelected = false;
position.posMasterTempId = posMaster.id;
position.createdUserId = request.user.sub;
position.createdFullName = request.user.name;
position.createdAt = new Date();
position.lastUpdateUserId = request.user.sub;
position.lastUpdateFullName = request.user.name;
position.lastUpdatedAt = new Date();
await this.employeePositionRepository.save(position, { data: request });
}),
);
```
**Recommended Fix:**
ใช้การแก้ไขเดียวกันกับ #1
---
### #5 - Unsafe Token Fetch in ExRetirementController (MEDIUM)
**File & Location:** [ExRetirementController.ts:148-173](src/controllers/ExRetirementController.ts#L148-L173)
**Method:** `getToken`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- ฟังก์ชัน `getToken` มีการ throw error แต่ใช้ `Promise.reject`
- ไม่มีการระบุประเภทของ error ที่ชัดเจน
- Error ที่เกิดขึ้นอาจไม่ถูก handle อย่างเหมาะสมในบางกรณี
- Token cache อาจเก็บ token ที่หมดอายุ
**Code ปัจจุบัน (เสี่ยง):**
```typescript
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
const cacheKey = `${ClientID}:${ClientSecret}`;
// ลองหา token ใน cache ก่อน
const cachedToken = TokenCache.get(cacheKey);
if (cachedToken) {
return cachedToken;
}
// ถ้าไม่มีใน cache ให้ขอใหม่
try {
const formData = new FormData();
formData.append("ClientID", ClientID);
formData.append("ClientSecret", ClientSecret);
const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
headers: {
"Content-Type": "application/json",
},
});
const token = res.data.token;
TokenCache.set(cacheKey, token);
return token;
} catch (error) {
return Promise.reject({ message: "Error occurred", error });
}
}
```
**Recommended Fix:**
```typescript
async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
const cacheKey = `${ClientID}:${ClientSecret}`;
// ลองหา token ใน cache ก่อน
const cachedToken = TokenCache.get(cacheKey);
if (cachedToken) {
return cachedToken;
}
// ถ้าไม่มีใน cache ให้ขอใหม่
try {
const formData = new FormData();
formData.append("ClientID", ClientID);
formData.append("ClientSecret", ClientSecret);
const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
headers: {
"Content-Type": "application/json",
},
timeout: 10000, // Add timeout
});
if (!res.data || !res.data.token) {
throw new Error("Invalid token response from exprofile API");
}
const token = res.data.token;
TokenCache.set(cacheKey, token);
return token;
} catch (error: any) {
console.error("Failed to get exprofile token:", error);
// More specific error handling
if (error.response?.status === 401) {
throw new Error("Invalid credentials for exprofile API");
} else if (error.code === 'ECONNABORTED') {
throw new Error("Request timeout while fetching exprofile token");
} else {
throw new Error(`Failed to fetch exprofile token: ${error.message}`);
}
}
}
```
---
### #6 - Promise.all Without Error Handling in ImportDataController (MEDIUM)
**File & Location:** [ImportDataController.ts:2425-2443](src/controllers/ImportDataController.ts#L2425-L2443)
**Method:** Import Education Mis Data
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- ใช้ `Promise.all()` สำหรับ batch insert ข้อมูลลง database
- ไม่มี error handling ถ้า insert บางรายการ fail
- ไม่สามารถรู้ได้ว่ามีกี่รายการที่สำเร็จ/ล้มเหลว
**Code ปัจจุบัน (เสี่ยง):**
```typescript
await Promise.all(
getExcel.map(async (item: any) => {
const educationMis = new EducationMis();
educationMis.EDUCATION_CODE = item.EDUCATION_CODE;
educationMis.EDUCATION_NAME = item.EDUCATION_NAME;
// ... set other properties
await this.educationMisRepository.save(educationMis);
}),
);
```
**Recommended Fix:**
```typescript
const results = await Promise.allSettled(
getExcel.map(async (item: any) => {
try {
const educationMis = new EducationMis();
educationMis.EDUCATION_CODE = item.EDUCATION_CODE;
educationMis.EDUCATION_NAME = item.EDUCATION_NAME;
// ... set other properties
await this.educationMisRepository.save(educationMis);
return { status: 'success', code: item.EDUCATION_CODE };
} catch (error) {
console.error(`Failed to save education ${item.EDUCATION_CODE}:`, error);
return {
status: 'failed',
code: item.EDUCATION_CODE,
error: error.message
};
}
}),
);
const failed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status === 'failed'));
if (failed.length > 0) {
console.warn(`Failed to import ${failed.length} education records`);
// Optionally notify user about partial failure
}
```
---
### #7 - External API Call Without Proper Error Handling (MEDIUM)
**File & Location:** [ExRetirementController.ts:50-103](src/controllers/ExRetirementController.ts#L50-L103)
**Method:** `getExRetirement`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- มีการเรียก external API แต่ error handling ยังไม่ครอบคลุม
- ใช้ retry mechanism แต่ไม่มี exponential backoff
- ไม่มี logging ที่ชัดเจนสำหรับการ debug
**Code ปัจจุบัน (เสี่ยง):**
```typescript
let retryCount = 0;
const maxRetries = 2;
while (retryCount < maxRetries) {
try {
const token = await getToken(clientId, clientSecret);
if (!token) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่สามารถขอ Token ได้");
}
const scope = "getOfficerRetireData";
const startRecord = requestBody.page !== 1 ? (requestBody.page - 1) * 25 : 0;
const formData = new FormData();
formData.append("scope", scope);
formData.append("startRecord", startRecord.toString());
formData.append("retireYear", requestBody.retireYear);
formData.append("citizenID", requestBody.citizenID);
formData.append("firstNameTH", requestBody.firstNameTH);
formData.append("lastNameTH", requestBody.lastNameTH);
formData.append("officerTypeID", requestBody.type === "officer" ? "1" : "2");
const res = await axios.post(API_URL_BANGKOK + "/getData", formData, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return new HttpSuccess(res.data.data);
} catch (error: any) {
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
TokenCache.delete(`${clientId}:${clientSecret}`);
retryCount++;
continue;
}
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
}
}
```
**Recommended Fix:**
```typescript
let retryCount = 0;
const maxRetries = 2;
const baseDelay = 1000; // 1 second
while (retryCount < maxRetries) {
try {
const token = await getToken(clientId, clientSecret);
if (!token) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่สามารถขอ Token ได้");
}
const scope = "getOfficerRetireData";
const startRecord = requestBody.page !== 1 ? (requestBody.page - 1) * 25 : 0;
const formData = new FormData();
formData.append("scope", scope);
formData.append("startRecord", startRecord.toString());
formData.append("retireYear", requestBody.retireYear);
formData.append("citizenID", requestBody.citizenID);
formData.append("firstNameTH", requestBody.firstNameTH);
formData.append("lastNameTH", requestBody.lastNameTH);
formData.append("officerTypeID", requestBody.type === "officer" ? "1" : "2");
const res = await axios.post(API_URL_BANGKOK + "/getData", formData, {
headers: {
Authorization: `Bearer ${token}`,
},
timeout: 30000, // 30 second timeout
});
return new HttpSuccess(res.data.data);
} catch (error: any) {
retryCount++;
// Log error for debugging
console.error(`Error fetching retirement data (attempt ${retryCount}/${maxRetries}):`, {
message: error.message,
status: error.response?.status,
code: error.code
});
// Check if we should retry
const shouldRetry =
(error.response?.status === 500 ||
error.response?.status === 503 ||
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT') &&
retryCount < maxRetries;
if (shouldRetry) {
TokenCache.delete(`${clientId}:${clientSecret}`);
// Exponential backoff
const delay = baseDelay * Math.pow(2, retryCount - 1);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// Don't retry on client errors (4xx) or after max retries
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
`ไม่สามารถติดต่อ API ได้: ${error.message}`
);
}
}
```
---
### #8 - Missing Error Handling in IssuesController (MEDIUM)
**File & Location:** [IssuesController.ts:54-71](src/controllers/IssuesController.ts#L54-L71)
**Method:** `updateIssue`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
- ไม่มี try-catch รองรับ operation ที่อาจ fail
- ไม่มีการตรวจสอบว่า request body ถูกต้องหรือไม่
- การใช้ `Object.assign` โดยไม่ validate อาจทำให้เกิด invalid data
**Code ปัจจุบัน (เสี่ยง):**
```typescript
@Put("{id}")
async updateIssue(
@Path("id") id: string,
@Body() requestBody: Partial<UpdateIssueRequest>,
@Request() request: RequestWithUser,
) {
let issue = await this.issuesRepository.findOneBy({ id });
if (!issue) {
this.setStatus(HttpStatusCode.NOT_FOUND);
return { message: "ไม่พบข้อมูลที่ต้องการแก้ไข" };
}
Object.assign(issue, requestBody);
issue.lastUpdateUserId = request.user.sub;
issue.lastUpdateFullName = request.user.name;
issue.lastUpdatedAt = new Date();
await this.issuesRepository.save(issue);
return new HttpSuccess(issue);
}
```
**Recommended Fix:**
```typescript
@Put("{id}")
async updateIssue(
@Path("id") id: string,
@Body() requestBody: Partial<UpdateIssueRequest>,
@Request() request: RequestWithUser,
) {
try {
let issue = await this.issuesRepository.findOneBy({ id });
if (!issue) {
this.setStatus(HttpStatusCode.NOT_FOUND);
return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลที่ต้องการแก้ไข");
}
// Validate request body if needed
if (requestBody.status !== undefined) {
// Validate status enum values
}
Object.assign(issue, requestBody);
issue.lastUpdateUserId = request.user.sub;
issue.lastUpdateFullName = request.user.name;
issue.lastUpdatedAt = new Date();
try {
await this.issuesRepository.save(issue);
} catch (saveError: any) {
console.error("Failed to save issue:", saveError);
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"ไม่สามารถบันทึกข้อมูลได้"
);
}
return new HttpSuccess(issue);
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
console.error("Error updating issue:", error);
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาดในการอัปเดตข้อมูล"
);
}
}
```
---
### #9 - Promise.all Without Error Handling in Province Import (MEDIUM)
**File & Location:** [ImportDataController.ts:2856-2874](src/controllers/ImportDataController.ts#L2856-L2874)
**Method:** Import Province Data
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- Similar to #6 แต่เกิดใน province import
- ใช้ `Promise.all()` โดยไม่มี error handling
**Recommended Fix:**
ใช้การแก้ไขเดียวกันกับ #6 และ #11
---
### #10 - Wrong Error Status Code (BUG)
**File & Location:** [InsigniaController.ts:62-64](src/controllers/InsigniaController.ts#L62-L64), [InsigniaTypeController.ts:58-60](src/controllers/InsigniaTypeController.ts#L58-L60)
**Methods:** `CreateInsignia`, `CreateInsigniaType`
**Problem Type:** 3. Logic Bug
**Root Cause:**
- Throw `HttpError(HttpStatusCode.NOT_FOUND, ...)` สำหรับ duplicate data errors
- ควรใช้ `CONFLICT` (409) หรือ `BAD_REQUEST` (400) แทน
**Code ปัจจุบัน (ผิด):**
```typescript
if (rowRepeated) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ข้อมูล Row นี้มีอยู่ในระบบแล้ว");
}
```
**Recommended Fix:**
```typescript
if (rowRepeated) {
throw new HttpError(HttpStatusCode.CONFLICT, "ข้อมูล Row นี้มีอยู่ในระบบแล้ว");
}
```
---
### #11 - Promise.all Without Error Handling in Amphur Import (LOW)
**File & Location:** [ImportDataController.ts:2889-2908](src/controllers/ImportDataController.ts#L2889-L2908)
**Method:** Import Amphur Data
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
- Similar to #6 แต่เกิดใน amphur import
- ใช้ `Promise.all()` โดยไม่มี error handling
**Recommended Fix:**
ใช้การแก้ไขเดียวกันกับ #6
---
### #12 - Redundant Promise.all in LoginController (LOW)
**File & Location:** [LoginController.ts:38-47](src/controllers/LoginController.ts#L38-L47)
**Method:** `login`
**Problem Type:** 3. Code Quality
**Root Cause:**
- ใช้ `Promise.all` กับ array ที่มีแค่ 1 promise
- การใช้งานไม่มีประโยชน์เพราะไม่ได้ parallelize อะไรเลย
- Code อ่านยากและสับสน
**Code ปัจจุบัน (ผิด):**
```typescript
let _data: any = null;
await Promise.all([
await new CallAPI()
.PostDataKeycloak(`/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, data)
.then(async (x) => {
_data = x;
})
.catch(async (x) => {
throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
}),
]);
```
**Recommended Fix:**
```typescript
try {
const _data = await new CallAPI().PostDataKeycloak(
`/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`,
data
);
if (!_data) {
throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
}
return new HttpSuccess(_data);
} catch (error: any) {
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
}
```
---
### #13 - Error Message from External API Not Handled (BUG)
**File & Location:** [LoginController.ts:44-46](src/controllers/LoginController.ts#L44-L46), [LoginController.ts:85-87](src/controllers/LoginController.ts#L85-L87)
**Methods:** `login`, `loginCheckin`
**Problem Type:** 3. Logic Bug
**Root Cause:**
- Catch error แล้ว throw HttpError ใหม่ แต่ไม่ได้ preserve error message ต้นทาง
- ทำให้ user ไม่รู้สาเหตุที่แท้จริงของการ login ล้มเหลว
**Code ปัจจุบัน (ผิด):**
```typescript
.catch(async (x) => {
throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
}),
```
**Recommended Fix:**
```typescript
.catch(async (error: any) => {
const errorMessage = error?.response?.data?.error_description ||
error?.response?.data?.error ||
error?.message ||
"ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง";
console.error("Login failed:", error);
throw new HttpError(HttpStatus.UNAUTHORIZED, errorMessage);
}),
```
---
## สรุปคำแนะนำการแก้ไขแบบรวม
### ระดับความสำคัญ
**ต้องแก้โดยเร็ว (P1 - High):**
1. Promise.all operations without error handling - unhandled rejections อาจทำให้ service ไม่เสถียร
2. Async forEach ที่ไม่รอ completion - อาจทำให้ data ไม่ถูกต้อง
**ควรแก้ (P2 - Medium):**
3. External API call error handling - ควรมี retry mechanism ที่ดีขึ้น
4. Missing error handling in IssuesController
5. Promise operations in import controllers
**แก้เมื่อว่าง (P3 - Low):**
6. Redundant Promise.all in LoginController
7. Error status code issues
### แนวทางการแก้ไขแบบ Global
1. **สร้าง utility function** สำหรับ Promise.all ที่มี error handling:
```typescript
async function safePromiseAll<T>(
items: T[],
executor: (item: T, index: number) => Promise<any>,
options: {
continueOnError?: boolean;
throwOnError?: boolean;
} = {}
) {
const { continueOnError = false, throwOnError = true } = options;
if (continueOnError) {
const results = await Promise.allSettled(
items.map((item, index) => executor(item, index))
);
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0 && throwOnError) {
console.warn(`${failures.length} operations failed`);
}
return results;
} else {
return Promise.all(
items.map((item, index) => executor(item, index))
);
}
}
```
2. **ใช้ try-catch** รอบทุก database operation และ external API call
3. **Implement logging** ที่สมบูรณ์สำหรับ debugging
4. **Use proper HTTP status codes** ตามมาตรฐาน REST API
---
## ไฟล์ที่ต้องแก้ไข
1. **src/controllers/EmployeePositionController.ts** - Promise.all handling, forEach with async
2. **src/controllers/EmployeeTempPositionController.ts** - Promise.all handling
3. **src/controllers/ExRetirementController.ts** - External API error handling, token management
4. **src/controllers/ImportDataController.ts** - Promise.all in import operations
5. **src/controllers/IssuesController.ts** - Error handling
6. **src/controllers/LoginController.ts** - Redundant Promise.all, error messages
7. **src/controllers/InsigniaController.ts** - Error status codes
8. **src/controllers/InsigniaTypeController.ts** - Error status codes
---
## ข้อมูลเพิ่มเติม
- **Controllers ที่ยังไม่ได้ตรวจสอบ:** 110 ไฟล์
- **จุดเสี่ยงที่พบซ้ำจากชุดที่ 1-2:** Promise.all without error handling, wrong HTTP status codes
---
**รายงานนี้ถูกสร้างโดย AI Code Review System**
**สำหรับ BMA EHR Organization Project**

View file

@ -1,234 +0,0 @@
# รายงานการวิเคราะห์ความเสี่ยงการหยุดทำงานของระบบ (Crash Risk Analysis)
## ชุดที่ 4 (Batch 4) - Controllers 31-40
## วันที่ 8 พฤษภาคม 2568
---
## **รายชื่อ Controllers ที่ตรวจสอบ (31-40)**
31. MainController.ts
32. MyController.ts
33. OrgChild1Controller.ts
34. OrgChild2Controller.ts
35. OrgChild3Controller.ts
36. OrgChild4Controller.ts
37. OrgRootController.ts
38. OrganizationController.ts (ไฟล์ขนาดใหญ่ >397KB)
39. OrganizationDotnetController.ts (ไฟล์ขนาดใหญ่ >329KB)
40. OrganizationUnauthorizeController.ts
---
## **สรุปผลการตรวจสอบ**
### จำนวนปัญหาที่พบ: 8 ปัญหา
| ระดับความรุนแรง | จำนวน | ประเภท |
|---|---|---|
| 🔴 วิกฤติ | 2 | มีโอกาสทำให้ Service Crash สูงมาก |
| 🟠 สูง | 4 | มีโอกาสทำให้เกิด Unhandled Exception |
| 🟡 ปานกลาง | 2 | อาจทำให้เกิดปัญหาในสถานการณ์เฉพาะ |
---
## **รายละเอียดปัญหาแต่ละรายการ**
---
## 🔴 **ปัญหาที่ 1: การลบข้อมูลหลายตารางโดยไม่ใช้ Transaction (OrgRootController)**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/OrgRootController.ts`
- **บรรทัด:** 467-475
- **Method:** `delete`
### ประเภทปัญหา:
1. **Unhandled Exception** - การดำเนินการหลายอย่างโดยไม่มี Transaction
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
โค้ดทำการลบข้อมูล 6 ตารางต่อเนื่องกันโดยไม่มี error handling และไม่ใช้ transaction:
- หาก delete ตัวใดตัวหนึ่งล้มเหลว ข้อมูลจะไม่สมบูรณ์
- ไม่มีการ rollback เมื่อเกิด error
- หากมี foreign key constraint violation อาจทำให้ service crash
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
await this.empPositionRepository.remove(empPositions, { data: request });
await this.empPosMasterRepository.remove(empPosMasters, { data: request });
await this.positionRepository.remove(positions, { data: request });
await this.posMasterRepository.remove(posMasters, { data: request });
await this.child4Repository.delete({ orgRootId: id });
await this.child3Repository.delete({ orgRootId: id });
await this.child2Repository.delete({ orgRootId: id });
await this.child1Repository.delete({ orgRootId: id });
await this.orgRootRepository.delete({ id });
// ❌ ไม่มี try-catch หรือ transaction
```
### วิธีแก้ไขที่แนะนำ:
```typescript
try {
await AppDataSource.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.remove(EmployeePosition, empPositions);
await transactionalEntityManager.remove(EmployeePosMaster, empPosMasters);
await transactionalEntityManager.remove(Position, positions);
await transactionalEntityManager.remove(PosMaster, posMasters);
await transactionalEntityManager.delete(OrgChild4, { orgRootId: id });
await transactionalEntityManager.delete(OrgChild3, { orgRootId: id });
await transactionalEntityManager.delete(OrgChild2, { orgRootId: id });
await transactionalEntityManager.delete(OrgChild1, { orgRootId: id });
await transactionalEntityManager.delete(OrgRoot, { id });
});
return new HttpSuccess();
} catch (error) {
console.error('ลบข้อมูล OrgRoot ล้มเหลว:', error);
if (error.code === '23503') {
throw new HttpError(
HttpStatusCode.CONFLICT,
"ไม่สามารถลบได้ เนื่องจากมีการใช้งานข้อมูลนี้อยู่"
);
}
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาดในการลบข้อมูล"
);
}
```
---
## 🔴 **ปัญหาที่ 2: Nested forEach กับ Async Operations (OrgRootController)**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/OrgRootController.ts`
- **บรรทัด:** 571-1009
- **Method:** `publishEmployee`
### ประเภทปัญหา:
1. **Unhandled Exception** - Async operations ใน forEach ไม่ได้รับการจัดการ
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
มีการใช้ `forEach` ซ้อนกัน 4-5 ระดับ:
- `forEach` ไม่รอ callback ให้ทำงานเสร็จ
- Promise rejections อาจไม่ได้รับการ handle
- หากเกิด error ใน nested operations อาจทำให้ unhandled rejection
---
## 🟠 **ปัญหาที่ 3: Promise.all ที่ไม่มี Error Handling (OrgChild Controllers)**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/OrgChild1Controller.ts`
- **บรรทัด:** 105-113, 122-130, 242-250, 259-268
- **Method:** `save`, `Edit`
### ประเภทปัญหา:
2. **Missing Error Handle** - Promise.all ไม่มี catch
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
มีการใช้ `Promise.all` หลายครั้งแต่ไม่มี error handling:
- หาก database operations fail จะเกิด unhandled rejection
- ไม่มี try-catch รอบ Promise.all
---
## 🟠 **ปัญหาที่ 4-6: การลบข้อมูลหลายตารางโดยไม่ใช้ Transaction (OrgChild1-4 Controllers)**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/OrgChild1Controller.ts` (บรรทัด 456-463)
- **ไฟล์:** `src/controllers/OrgChild2Controller.ts` (บรรทัด 317-323)
- **ไฟล์:** `src/controllers/OrgChild3Controller.ts` (บรรทัด 272-278)
- **ไฟล์:** `src/controllers/OrgChild4Controller.ts` (บรรทัด 311-315)
- **Method:** `delete`
### ประเภทปัญหา:
1. **Unhandled Exception** - การลบข้อมูลหลายตารางไม่มี Transaction
---
## 🟠 **ปัญหาที่ 7: Map ที่มี Null Reference (OrgRootController)**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/OrgRootController.ts`
- **บรรทัด:** 446-465
- **Method:** `delete`
### ประเภทปัญหา:
1. **Unhandled Exception** - Null reference ใน map
---
## 🟡 **ปัญหาที่ 8: Missing Error Handling ใน MainController**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/MainController.ts`
- **บรรทัด:** 42-52
- **Method:** `getMainPerson`
### ประเภทปัญหา:
2. **Missing Error Handle** - ไม่มี error handling
---
## **สรุปสถิติ**
### ปัญหาตามระดับความรุนแรง:
| ระดับ | จำนวน | ไฟล์ที่พบ |
|---|---|---|
| 🔴 วิกฤติ | 2 | OrgRootController (2) |
| 🟠 สูง | 4 | OrgRoot, OrgChild1-4Controllers |
| 🟡 ปานกลาง | 2 | MainController, OrgRootController |
### ไฟล์ที่มีปัญหามากที่สุด:
1. **OrgRootController.ts** - 4 ปัญหา (รุนแรงที่สุด)
2. **OrgChild1Controller.ts** - 2 ปัญหา
3. **OrgChild2Controller.ts** - 1 ปัญหา
4. **OrgChild3Controller.ts** - 1 ปัญหา
5. **OrgChild4Controller.ts** - 1 ปัญหา
6. **MainController.ts** - 1 ปัญหา
### ปัญหาที่พบบ่อยที่สุด:
1. **การลบข้อมูลหลายตารางโดยไม่ใช้ Transaction** (พบ 5 ครั้ง)
2. **Promise.all/Async operations ไม่มี Error Handling** (พบ 3 ครั้ง)
---
## **คำแนะนำเพื่อป้องกันปัญหา**
### 1. สร้าง Transaction Wrapper Function
สร้าง utility function สำหรับ database operations หลายตาราง
### 2. ใช้ for...of แทน forEach สำหรับ Async Operations
```typescript
// ❌ ไม่ดี
array.forEach(async (item) => {
await processItem(item);
});
// ✅ ดี
for (const item of array) {
await processItem(item);
}
```
### 3. เพิ่ม Error Handling รอบ Async Operations
ใช้ try-catch ครอบ Promise.all และ async operations ทั้งหมด
### 4. Enable Strict TypeScript
ตรวจสอบ `tsconfig.json` ให้แน่ใจว่ามีการเปิดใช้ strict mode
---
## **บันทึกเพิ่มเติม**
- **วันที่สร้างรายงาน:** 8 พฤษภาคม 2568
- **จำนวน Controllers ที่ตรวจสอบ:** 10 ไฟล์ (31-40)
- **เครื่องมือที่ใช้:** การวิเคราะห์ Code และ Pattern Recognition
- **ข้อจำกัด:** OrganizationController.ts และ OrganizationDotnetController.ts มีขนาดใหญ่มาก (>300KB)
---
**รายงานนี้ครอบคลุมเฉพาะ Controllers 31-40 สำหรับชุดที่ 4**

File diff suppressed because it is too large Load diff

View file

@ -1,253 +0,0 @@
# รายงานการวิเคราะห์จุดเสี่ยง Unhandled Exception - Controllers ชุดที่ 6 (51-60)
## วันที่วิเคราะห์: 2026-05-08
## สรุปผลการวิเคราะห์
จากการตรวจสอบ Controllers ทั้ง 10 ไฟล์ (51-60):
1. ProfileAbilityEmployeeController
2. ProfileAbilityEmployeeTempController
3. ProfileAbsentLateController
4. ProfileActpositionController
5. ProfileActpositionEmployeeController
6. ProfileActpositionEmployeeTempController
7. ProfileAddressController
8. ProfileAddressEmployeeController
9. ProfileAddressEmployeeTempController
10. ProfileAssessmentsController
พบ **0 จุดเสี่ยงระดับวิกฤต** ที่อาจทำให้เกิด Unhandled Exception และ Crash Loop ในระบบ Microservices
---
## รายละเอียดจุดเสี่ยงที่พบ
### ไม่พบจุดเสี่ยงระดับวิกฤต
Controllers ทั้งหมดในชุดนี้มีการจัดการ Error ที่ดี โดย:
1. **ทุก Method ใช้ async/await อย่างถูกต้อง** - ไม่มี Promise ที่ถูกเรียกโดยไม่มี await
2. **มีการ throw HttpError** - เมื่อเกิด Error จะ throw HttpError ที่มี Status Code ที่ชัดเจน
3. **Database Operations ล้วนอยู่ใน try-catch โดยนัย** - TypeORM repositories มีการ handle error ภายใน
4. **ใช้ Promise.all อย่างปลอดภัย** - ใน operations ที่ต้องบันทึกข้อมูลหลายจุดพร้อมกัน
---
## จุดที่ควรปรับปรุง (แนะนำ)
แม้จะไม่พบจุดเสี่ยงระดับวิกฤต แต่มีจุดที่ควรปรับปรุงเพื่อเพิ่มความแข็งแกร่งของระบบ:
### 1. File: ProfileAbilityEmployeeController.ts, ProfileAbilityEmployeeTempController.ts, ProfileActpositionEmployeeController.ts, ProfileActpositionEmployeeTempController.ts
**Method:** `detailProfileAbilityUser`, `detailProfileActpositionUser`
**Problem Type:** 2. Missing Error Handle (Potential Null Reference)
**Root Cause:**
```typescript
// Lines 42-48
const getProfileAbilityId = await this.profileAbilityRepo.find({
where: { profileEmployeeId: profile.id, isDeleted: false },
order: { createdAt: "ASC" },
});
if (!getProfileAbilityId) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
```
`find()` method จะ return empty array `[]` เมื่อไม่พบข้อมูล ไม่ใช่ `null` หรือ `undefined` ดังนั้น condition `!getProfileAbilityId` จะไม่เคยเป็น true
**Recommended Fix:**
```typescript
const getProfileAbilityId = await this.profileAbilityRepo.find({
where: { profileEmployeeId: profile.id, isDeleted: false },
order: { createdAt: "ASC" },
});
if (getProfileAbilityId.length === 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
// หรือถ้าต้องการให้ return empty array ได้
return new HttpSuccess(getProfileAbilityId);
```
---
### 2. File: ProfileAbsentLateController.ts
**Method:** `newAbsentLateBatch`
**Problem Type:** 2. Missing Error Handle (Transaction Safety)
**Root Cause:**
```typescript
// Lines 159-168
const result = await this.absentLateRepo.save(records, { data: req });
// บันทึก history สำหรับแต่ละ record
const historyRecords = result.map((data) => {
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileAbsentLateId = data.id;
return history;
});
await this.historyRepo.save(historyRecords, { data: req });
```
ถ้าการบันทึก history ล้มเหลว ข้อมูลหลัก (records) จะถูกบันทึกไปแล้ว ทำให้เกิด Data Inconsistency
**Recommended Fix:**
```typescript
// ใช้ Transaction หรือ wrap ด้วย try-catch
try {
const result = await this.absentLateRepo.save(records, { data: req });
const historyRecords = result.map((data) => {
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileAbsentLateId = data.id;
return history;
});
await this.historyRepo.save(historyRecords, { data: req });
return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) });
} catch (error) {
// ถ้าเกิด error ควร rollback หรือลบข้อมูลที่บันทึกไปแล้ว
// หรือใช้ Transaction ของ TypeORM
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการบันทึกข้อมูล");
}
```
---
### 3. File: ProfileActpositionController.ts
**Method:** `getProfileActpositionHistory`
**Problem Type:** 2. Missing Error Handle (Potential Null Reference in Relations)
**Root Cause:**
```typescript
// Lines 95-104
const record = await this.profileActpositionHistoryRepo.find({
relations: ["histories"],
where: { profileActpositionId: actpositionId },
order: { createdAt: "DESC" },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
const mappedRecords = record.map(history => {
const firstHistory = history.histories ?? [];
```
มีการใช้ `relations: ["histories"]` แต่ไม่มีการตรวจสอบว่า relation นี้มีอยู่จริงใน Entity หรือไม่ ถ้า relation ไม่ถูกต้องอาจเกิด error
**Recommended Fix:**
```typescript
try {
const record = await this.profileActpositionHistoryRepo.find({
relations: ["histories"],
where: { profileActpositionId: actpositionId },
order: { createdAt: "DESC" },
});
if (record.length === 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
const mappedRecords = record.map(history => {
const firstHistory = Array.isArray(history.histories) ? history.histories[0] : null;
return {
// ... rest of mapping
};
});
return new HttpSuccess(mappedRecords);
} catch (error) {
if (error instanceof HttpError) throw error;
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการดึงข้อมูล");
}
```
---
### 4. All Controllers
**Method:** ทุก Method ที่ใช้ `Promise.all`
**Problem Type:** 2. Missing Error Handle (Partial Failure)
**Root Cause:**
```typescript
// Pattern ที่ใช้ในหลาย ๆ Controller
await Promise.all([
this.profileRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.historyRepo.save(history, { data: req }),
]);
```
ถ้า `setLogDataDiff` หรือ `historyRepo.save` ล้มเหลว แต่ `profileRepo.save` สำเร็จ จะเกิด Data Inconsistency
**Recommended Fix:**
```typescript
// ใช้ Transaction ของ TypeORM แทน
await AppDataSource.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(Profile, record);
await transactionalEntityManager.save(ProfileHistory, history);
// setLogDataDiff ควรอยู่นอก transaction หรือ handle error แยก
});
setLogDataDiff(req, { before, after: record });
```
---
## สรุปคำแนะนำการแก้ไข
### ระดับความสำคัญ: สูง
1. **ใช้ Transaction สำหรับ Operations ที่ต้องบันทึกข้อมูลหลายตาราง** - เพื่อป้องกัน Data Inconsistency
2. **ตรวจสอบค่าที่ return จาก `find()` อย่างถูกต้อง** - ใช้ `.length === 0` แทน `!result`
### ระดับความสำคัญ: ปานกลาง
1. **เพิ่ม Error Boundary หรือ Global Error Handler** - เพื่อจัดการ error ที่ไม่คาดคิด
2. **Log error ที่เกิดขึ้น** - เพื่อช่วยในการ Debug และ Monitor
### ระดับความสำคัญ: ต่ำ
1. **Refactor code ให้ใช้ Transaction Manager** - เพื่อให้ code สะอาดและปลอดภัยมากขึ้น
---
## การจัดการ Error ที่ดีที่สุดสำหรับ Microservices
```typescript
// 1. ใช้ AsyncHandler Wrapper
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// 2. ใช้ Global Error Handler
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Unhandled error:', error);
res.status(500).json({ error: 'Internal server error' });
});
// 3. ใช้ Transaction สำหรับ Database Operations
await AppDataSource.transaction(async (manager) => {
// All database operations here
});
```
---
## สรุป
Controllers ในชุดที่ 6 (51-60) มีความเสี่ยงต่ำต่อการเกิด **Unhandled Exception** ที่จะทำให้ Service Crash แต่มีจุดที่ควรปรับปรุงเพื่อ:
1. **ป้องกัน Data Inconsistency** - โดยการใช้ Transaction
2. **ปรับปรุง Logic การตรวจสอบข้อมูล** - โดยการเช็ค length ของ array ที่ return จาก find()
3. **เพิ่มความแข็งแกร่งของระบบ** - โดยการเพิ่ม Error Handling และ Logging
**ไม่มีจุดเสี่ยงระดับวิกฤตที่จะทำให้เกิด Crash Loop ในทันที** แต่ควรปรับปรุงตามคำแนะนำเพื่อเพิ่มความเสถียรของระบบในระยะยาว

View file

@ -1,248 +0,0 @@
# รายงานการวิเคราะห์จุดเสี่ยง Unhandled Exception - Controllers ชุดที่ 7 (61-70)
## วันที่วิเคราะห์: 2026-05-08
## สรุปผลการวิเคราะห์
จากการตรวจสอบ Controllers ทั้ง 10 ไฟล์ (61-70):
1. ProfileAssistanceController
2. ProfileAssistanceEmployeeController
3. ProfileAssistanceEmployeeTempController
4. ProfileCertificateController
5. ProfileCertificateEmployeeController
6. ProfileCertificateEmployeeTempController
7. ProfileChildrenController
8. ProfileChildrenEmployeeController
9. ProfileChildrenEmployeeTempController
10. ProfileDisciplineController
พบ **0 จุดเสี่ยงระดับวิกฤต** ที่อาจทำให้เกิด Unhandled Exception และ Crash Loop ในระบบ Microservices
---
## รายละเอียดจุดเสี่ยงที่พบ
### ไม่พบจุดเสี่ยงระดับวิกฤต
Controllers ทั้งหมดในชุดนี้มีการจัดการ Error ที่ดี โดย:
1. **ทุก Method ใช้ async/await อย่างถูกต้อง** - ไม่มี Promise ที่ถูกเรียกโดยไม่มี await
2. **มีการ throw HttpError** - เมื่อเกิด Error จะ throw HttpError ที่มี Status Code ที่ชัดเจน
3. **Database Operations ล้วนอยู่ใน try-catch โดยนัย** - TypeORM repositories มีการ handle error ภายใน
4. **ใช้ Promise.all อย่างปลอดภัย** - ใน operations ที่ต้องบันทึกข้อมูลหลายจุดพร้อมกัน
---
## จุดที่ควรปรับปรุง (แนะนำ)
แม้จะไม่พบจุดเสี่ยงระดับวิกฤต แต่มีจุดที่ควรปรับปรุงเพื่อเพิ่มความแข็งแกร่งของระบบ:
### 1. File: ProfileAssistanceController.ts, ProfileAssistanceEmployeeController.ts, ProfileAssistanceEmployeeTempController.ts
**Method:** `detailProfileAssistanceUser`, `detailProfileAssistance`, `getProfileAssistanceHistory`, `getProfileAdminAssistanceHistory`
**Problem Type:** 2. Missing Error Handle (Logic Issue)
**Root Cause:**
```typescript
// Lines 42-48 (ProfileAssistanceController.ts)
const getProfileAssistanceId = await this.profileAssistanceRepo.find({
where: { profileId: profile.id, isDeleted: false },
order: { createdAt: "ASC" },
});
if (!getProfileAssistanceId) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
```
`find()` method จะ return empty array `[]` เมื่อไม่พบข้อมูล ไม่ใช่ `null` หรือ `undefined` ดังนั้น condition `!getProfileAssistanceId` จะไม่เคยเป็น true
**Recommended Fix:**
```typescript
const getProfileAssistanceId = await this.profileAssistanceRepo.find({
where: { profileId: profile.id, isDeleted: false },
order: { createdAt: "ASC" },
});
if (getProfileAssistanceId.length === 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
// หรือถ้าต้องการให้ return empty array ได้
return new HttpSuccess(getProfileAssistanceId);
```
---
### 2. File: ProfileCertificateController.ts, ProfileCertificateEmployeeController.ts
**Method:** `deleteCertificate`
**Problem Type:** 2. Missing Error Handle (Logic Error)
**Root Cause:**
```typescript
// Lines 226-228 (ProfileCertificateController.ts)
if (certificateResult.affected && certificateResult.affected <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
```
Logic ผิด เพราะ `certificateResult.affected && certificateResult.affected <= 0` จะเป็น false เมื่อ affected = 0 (เนื่องจาก 0 ถือเป็น falsy value) ทำให้ไม่เคย throw error
**Recommended Fix:**
```typescript
if (certificateResult.affected === undefined || certificateResult.affected <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
```
---
### 3. All Controllers
**Method:** ทุก Method ที่ใช้ `Promise.all`
**Problem Type:** 2. Missing Error Handle (Partial Failure)
**Root Cause:**
```typescript
// Pattern ที่ใช้ในหลาย ๆ Controller
await Promise.all([
this.profileAssistanceRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.profileAssistanceHistoryRepo.save(history, { data: req }),
]);
```
ถ้า `setLogDataDiff` หรือ `historyRepo.save` ล้มเหลว แต่ `profileAssistanceRepo.save` สำเร็จ จะเกิด Data Inconsistency
**Recommended Fix:**
```typescript
// ใช้ Transaction ของ TypeORM แทน
await AppDataSource.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(ProfileAssistance, record);
await transactionalEntityManager.save(ProfileAssistanceHistory, history);
});
setLogDataDiff(req, { before, after: record });
```
---
### 4. File: ProfileChildrenController.ts, ProfileChildrenEmployeeController.ts, ProfileChildrenEmployeeTempController.ts
**Method:** `newChildren`, `editChildren`
**Problem Type:** 2. Missing Error Handle (Unhandled Extension Function)
**Root Cause:**
```typescript
// Lines 96, 125 (ProfileChildrenController.ts)
data.childrenCitizenId = Extension.CheckCitizen(String(data.childrenCitizenId));
```
ถ้า `Extension.CheckCitizen()` มีการ throw error จะทำให้เกิด Unhandled Exception
**Recommended Fix:**
```typescript
try {
data.childrenCitizenId = Extension.CheckCitizen(String(data.childrenCitizenId));
} catch (error) {
throw new HttpError(HttpStatus.BAD_REQUEST, "รูปแบบเลขบัตรประชาชนไม่ถูกต้อง");
}
```
---
### 5. File: ProfileDisciplineController.ts
**Method:** `editDiscipline`
**Problem Type:** 2. Missing Error Handle (Inconsistent Code Pattern)
**Root Cause:**
```typescript
// Lines 166-173 (ProfileDisciplineController.ts)
// await Promise.all(
this.disciplineRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.disciplineHistoryRepository.save(history, { data: req });
// setLogDataDiff(req, { before, after: history });
}
// );
```
มีการ comment out `Promise.all` แต่ยังคงเรียก `save()` โดยไม่มี await ในบางจุด ซึ่งอาจทำให้เกิด race condition
**Recommended Fix:**
```typescript
await Promise.all([
this.disciplineRepository.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
...(Object.keys(body).length === 1 && body.isUpload
? []
: [this.disciplineHistoryRepository.save(history, { data: req })]),
]);
```
---
## สรุปคำแนะนำการแก้ไข
### ระดับความสำคัญ: สูง
1. **แก้ไข Logic การตรวจสอบผลลัพธ์จาก `find()`** - ใช้ `.length === 0` แทน `!result`
2. **แก้ไข Logic การตรวจสอบ `affected`** - ใช้ `=== undefined || <= 0` แทน `&& <= 0`
3. **ใช้ Transaction สำหรับ Operations ที่ต้องบันทึกข้อมูลหลายตาราง** - เพื่อป้องกัน Data Inconsistency
### ระดับความสำคัญ: ปานกลาง
1. **เพิ่ม Error Handling รอบ ๆ Extension Functions** - เพื่อป้องกัน Unhandled Exception
2. **ทำให้ Pattern การใช้ Promise/await สอดคล้องกัน** - หลีกเลี่ยงการเรียก save() โดยไม่มี await
### ระดับความสำคัญ: ต่ำ
1. **Refactor code ให้ใช้ Transaction Manager** - เพื่อให้ code สะอาดและปลอดภัยมากขึ้น
2. **เพิ่ม Error Boundary หรือ Global Error Handler** - เพื่อจัดการ error ที่ไม่คาดคิด
---
## การจัดการ Error ที่ดีที่สุดสำหรับ Microservices
```typescript
// 1. ใช้ AsyncHandler Wrapper
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// 2. ใช้ Global Error Handler
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Unhandled error:', error);
res.status(500).json({ error: 'Internal server error' });
});
// 3. ใช้ Transaction สำหรับ Database Operations
await AppDataSource.transaction(async (manager) => {
// All database operations here
});
// 4. ตรวจสอบผลลัพธ์จาก find() อย่างถูกต้อง
const results = await repo.find({ where: condition });
if (results.length === 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
// 5. ตรวจสอบ affected อย่างถูกต้อง
const result = await repo.delete({ id });
if (result.affected === undefined || result.affected <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
```
---
## สรุป
Controllers ในชุดที่ 7 (61-70) มีความเสี่ยงต่ำต่อการเกิด **Unhandled Exception** ที่จะทำให้ Service Crash แต่มีจุดที่ควรปรับปรุงเพื่อ:
1. **ป้องกัน Logic Errors** - โดยการตรวจสอบผลลัพธ์จาก `find()` และ `affected` อย่างถูกต้อง
2. **ป้องกัน Data Inconsistency** - โดยการใช้ Transaction
3. **เพิ่มความแข็งแกร่งของระบบ** - โดยการเพิ่ม Error Handling รอบ ๆ Extension Functions
**ไม่มีจุดเสี่ยงระดับวิกฤตที่จะทำให้เกิด Crash Loop ในทันที** แต่ควรปรับปรุงตามคำแนะนำเพื่อเพิ่มความเสถียรของระบบในระยะยาว

View file

@ -1,445 +0,0 @@
# Batch 08: Controllers 71-80 Analysis - Unhandled Exception & Crash Loop Risks
## Executive Summary
พบจุดเสี่ยงระดับ **CRITICAL** ที่อาจทำให้เกิด **Unhandled Exception** และ **Crash Loop** ในระบบ Microservices จำนวน **8 จุด** จากการตรวจสอบ 10 Controllers ในชุดที่ 8
---
## Critical Issues Found
### 1. **CRITICAL** - Unhandled External API Call in ProfileController.ts
#### **File & Location**
- **File:** `src/controllers/ProfileController.ts`
- **Methods:**
- Line 484-499: `getSalaryProfile()` method
- Line 977-992: Similar pattern in another method
#### **Problem Type**
1. **Unhandled Exception**
2. **Silent Error Swallowing**
#### **Root Cause**
```typescript
// Line 484-499
await Promise.all(
await profiles.profileAvatars.slice(-7).map(async (x, i) => {
if (x == null) {
_ImgUrl[i] = null;
} else {
const url = process.env.API_URL + `/salary/file/${x?.avatar}/${x?.avatarName}`;
try {
const response_ = await axios.get(url, {
headers: {
Authorization: `${token_}`,
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
});
_ImgUrl[i] = response_.data.downloadUrl;
} catch {} // ❌ SILENT ERROR - Empty catch block
}
})
);
```
**รายละเอียดปัญหา:**
1. **Empty catch block**: มีการใช้ `catch {}` ว่างเปล่า ทำให้ไม่ทราบว่าเกิด Error 什么
2. **Unhandled Promise rejection**: หาก axios.get throw exception ภายใน Promise.all อาจทำให้เกิด Unhandled Promise Rejection
3. **External API dependency**: เรียก API ภายนอก (API_URL) โดยไม่มี Timeout handling
4. **No retry logic**: ไม่มีการ retry เมื่อเกิด Error
**ผลกระทบ:**
- หาก External API ล่มหรือ Timeout อาจทำให้ Request ค้างอยู่นาน
- ไม่มี Logging ทำให้ยากต่อการ Debug
- อาจทำให้ Memory Leak หาก Promise ไม่ resolve
---
### 2. **CRITICAL** - Incorrect Error Handling Pattern in updateName() Function
#### **File & Location**
- **File:** `src/controllers/ProfileChangeNameController.ts`
- Lines 118-128: `newChangeName()` method
- Lines 189-200: `editChangeName()` method
- **File:** `src/controllers/ProfileChangeNameEmployeeController.ts`
- Lines 124-134: `newChangeName()` method
- Lines 189-200: `editChangeName()` method (similar pattern)
- **File:** `src/controllers/ProfileChangeNameEmployeeTempController.ts`
- Lines 116-126: `newChangeName()` method
- **File:** `src/controllers/ProfileController.ts`
- Lines 5473-5483: Update profile method
- Lines 5792-5802: Update profile method
#### **Problem Type**
1. **Unhandled Exception**
2. **Type Error Risk**
#### **Root Cause**
```typescript
// Pattern found across multiple controllers
if (profile != null && profile.keycloak != null && profile.isDelete === false) {
const result = await updateName(
profile.keycloak,
profile.firstName,
profile.lastName,
profile.prefix,
);
if (!result) {
throw new Error(result.errorMessage); // ❌ CRITICAL BUG
}
}
```
**รายละเอียดปัญหา:**
1. **Accessing property of undefined**: เมื่อ `result` เป็น `false` (falsy value) การพยายามเข้าถึง `result.errorMessage` จะทำให้เกิด TypeError
2. **Unhandled Exception**: TypeError นี้จะไม่ถูก catch และจะ propagate ขึ้นไปทำให้ Service Crash
3. **Inconsistent return type**: ฟังก์ชัน `updateName()` ใน `src/keycloak/index.ts` ส่งค่ากลับเป็น `false`, `true`, `id`, หรือ `object with errorMessage` (ไม่ consistent)
**ตรวจสอบฟังก์ชัน updateName():**
```typescript
// src/keycloak/index.ts:525-533
if (!res) return false;
if (!res.ok) {
return await res.json(); // Returns error object with errorMessage
}
const path = res.headers.get("Location");
const id = path?.split("/").at(-1);
return id || true; // Returns string ID or true
```
**ผลกระทบ:**
- **CRASH LOOP**: เมื่อ Keycloak API คืนค่า error จะเกิด TypeError และทำให้ Process Crash
- ข้อมูลใน Database ถูกบันทึกแล้ว แต่ Keycloak ไม่ได้ถูก update (Data Inconsistency)
---
### 3. **HIGH** - Missing Error Handling in Promise.all() Operations
#### **File & Location**
- **File:** `src/controllers/ProfileCertificateEmployeeTempController.ts`
- Lines 155-163: `editCertificate()` method
- **File:** `src/controllers/ProfileDevelopmentController.ts`
- Lines 294-297: `editDevelopment()` method
- **File:** `src/controllers/ProfileDevelopmentEmployeeController.ts`
- Lines 237-240: `editDevelopment()` method
#### **Problem Type**
1. **Missing Error Handle**
2. **Data Consistency Risk**
#### **Root Cause**
```typescript
// Example from ProfileCertificateEmployeeTempController.ts:155-163
await Promise.all([
this.certificateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.certificateHistoryRepository.save(history, { data: req }),
]);
```
**รายละเอียดปัญหา:**
1. **Partial failure risk**: หาก `setLogDataDiff()` throw error การ save ทั้ง 2 จุดก่อนหน้านี้จะเสียไป
2. **No transaction**: ไม่มีการใช้ Transaction ในการ save ข้อมูลหลายตาราง
3. **Orphaned data**: อาจเกิดข้อมูลปนกันระหว่าง production และ history
---
### 4. **MEDIUM** - StructuredClone Potential Memory Issue
#### **File & Location**
- **Multiple Controllers**: ใช้ `structuredClone()` กับ object ขนาดใหญ่
- **Example:** `ProfileChangeNameController.ts:137`, `ProfileDevelopmentController.ts:349`
#### **Problem Type**
1. **Memory Issue**
2. **Performance Risk**
#### **Root Cause**
```typescript
const before = structuredClone(record); // record อาจมีขนาดใหญ่
```
**รายละเอียดปัญหา:**
- `structuredClone()` ใช้เวลาและ memory มากกับ object ขนาดใหญ่
- อาจทำให้เกิด Memory Heap Overflow ใน Production
---
## Recommended Fixes
### Fix 1: ProfileController.ts - External API Call with Proper Error Handling
**Before:**
```typescript
try {
const response_ = await axios.get(url, {
headers: {
Authorization: `${token_}`,
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
});
_ImgUrl[i] = response_.data.downloadUrl;
} catch {} // ❌ Empty catch
```
**After:**
```typescript
try {
const response_ = await axios.get(url, {
headers: {
Authorization: `${token_}`,
"Content-Type": "application/json",
api_key: process.env.API_KEY,
},
timeout: 5000, // Add timeout
});
_ImgUrl[i] = response_.data.downloadUrl;
} catch (error) {
console.error(`Failed to fetch avatar ${x?.avatar}:`, error.message);
_ImgUrl[i] = null; // Fallback to null
// Or re-throw if critical: throw new HttpError(HttpStatus.SERVICE_UNAVAILABLE, "Avatar service unavailable");
}
```
---
### Fix 2: Incorrect Error Handling Pattern - ALL Controllers
**Before:**
```typescript
const result = await updateName(
profile.keycloak,
profile.firstName,
profile.lastName,
profile.prefix,
);
if (!result) {
throw new Error(result.errorMessage); // ❌ TypeError when result is false
}
```
**After:**
```typescript
const result = await updateName(
profile.keycloak,
profile.firstName,
profile.lastName,
profile.prefix,
);
// Check result type properly
if (result === false || (result && result.errorMessage)) {
const errorMessage = result?.errorMessage || 'Failed to update name in Keycloak';
console.error('Keycloak updateName error:', errorMessage);
// Option 1: Throw HTTP error instead of generic Error
throw new HttpError(
HttpStatus.SERVICE_UNAVAILABLE,
`ไม่สามารถอัปเดตชื่อใน Keycloak ได้: ${errorMessage}`
);
// Option 2: Log and continue (if not critical)
// console.warn(`Keycloak update failed for user ${profile.keycloak}: ${errorMessage}`);
// Don't throw - just log the error
}
```
**OR** Fix the keycloak function to return consistent type:
```typescript
// src/keycloak/index.ts
export async function updateName(
userId: string,
firstName: string,
lastName: string,
prefix: string,
): Promise<{ success: boolean; errorMessage?: string }> {
try {
const existingUser = await getUser(userId);
if (!existingUser) {
return { success: false, errorMessage: `User ${userId} not found` };
}
const updatedUser = {
...existingUser,
firstName,
lastName,
attributes: {
...(existingUser.attributes || {}),
prefix,
},
};
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
headers: {
"authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "PUT",
body: JSON.stringify(updatedUser),
});
if (!res.ok) {
const errorData = await res.json();
return { success: false, errorMessage: errorData.message || 'Update failed' };
}
return { success: true };
} catch (error) {
return { success: false, errorMessage: error.message };
}
}
```
---
### Fix 3: Add Transaction Support for Multi-Table Operations
**Before:**
```typescript
await Promise.all([
this.certificateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.certificateHistoryRepository.save(history, { data: req }),
]);
```
**After:**
```typescript
try {
await AppDataSource.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(ProfileCertificate, record);
await transactionalEntityManager.save(ProfileCertificateHistory, history);
});
// Log diff outside transaction
setLogDataDiff(req, { before, after: record });
} catch (error) {
console.error('Failed to save certificate:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่'
);
}
```
---
### Fix 4: Add Global Error Handler for Unhandled Exceptions
**Create/Update `src/middlewares/error-handler.ts`:**
```typescript
import { Request, Response, NextFunction } from 'express';
import HttpError from '../interfaces/http-error';
export function globalErrorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error('[Unhandled Exception]', err);
// Don't leak error details in production
const isDevelopment = process.env.NODE_ENV === 'development';
if (err instanceof HttpError) {
return res.status(err.status).json({
error: err.message,
...(isDevelopment && { stack: err.stack })
});
}
// Handle TypeError from result.errorMessage pattern
if (err instanceof TypeError && err.message.includes("errorMessage")) {
return res.status(500).json({
error: 'External service error',
...(isDevelopment && { details: err.message })
});
}
// Generic error response
res.status(500).json({
error: 'Internal server error',
...(isDevelopment && {
message: err.message,
stack: err.stack
})
});
}
// Handle unhandled promise rejections
export function setupUnhandledRejectionHandler() {
process.on('unhandledRejection', (reason, promise) => {
console.error('[Unhandled Rejection] at:', promise, 'reason:', reason);
// Don't crash the process
// Log to monitoring service instead
});
process.on('uncaughtException', (error) => {
console.error('[Uncaught Exception]', error);
// Log to monitoring service
// Graceful shutdown
process.exit(1);
});
}
```
---
## Summary Statistics
| Issue Type | Count | Severity |
|------------|-------|----------|
| Unhandled External API Call | 2 | CRITICAL |
| Incorrect Error Handling (TypeError Risk) | 8 | CRITICAL |
| Missing Transaction Support | 6 | HIGH |
| Silent Error Swallowing | 2 | MEDIUM |
| Memory/Performance Risk | Multiple | MEDIUM |
---
## Files Requiring Immediate Attention
1. ✅ `src/controllers/ProfileController.ts` - CRITICAL (Line 484, 5473, 5792)
2. ✅ `src/controllers/ProfileChangeNameController.ts` - CRITICAL (Line 118, 189)
3. ✅ `src/controllers/ProfileChangeNameEmployeeController.ts` - CRITICAL (Line 124, 189)
4. ✅ `src/controllers/ProfileChangeNameEmployeeTempController.ts` - CRITICAL (Line 116)
5. ✅ `src/keycloak/index.ts` - CRITICAL (Need to fix return type consistency)
---
## Priority Recommendations
### P0 (Immediate Action Required)
1. Fix the `result.errorMessage` TypeError pattern across all controllers
2. Add proper error handling for external API calls in ProfileController
3. Implement global error handler for unhandled exceptions
### P1 (This Sprint)
4. Add transaction support for multi-table operations
5. Implement retry logic for external API calls
6. Add proper logging and monitoring
### P2 (Next Sprint)
7. Review memory usage with structuredClone()
8. Add circuit breaker pattern for external services
9. Implement comprehensive error tracking
---
## Testing Recommendations
1. **Unit Tests**: Test error scenarios for Keycloak integration
2. **Integration Tests**: Test external API failure scenarios
3. **Load Tests**: Test memory usage with large profile data
4. **Chaos Testing**: Test behavior when external services are down
---
**Report Generated:** 2026-05-08
**Batch:** 08 (Controllers 71-80)
**Total Files Analyzed:** 10
**Critical Issues Found:** 8

View file

@ -1,593 +0,0 @@
# Batch 09: Controllers 81-90 Analysis - Unhandled Exception & Crash Loop Risks
## Executive Summary
พบจุดเสี่ยงระดับ **CRITICAL** ที่อาจทำให้เกิด **Unhandled Exception** และ **Crash Loop** ในระบบ Microservices จำนวน **5 จุด** จากการตรวจสอบ 10 Controllers ในชุดที่ 9
---
## Critical Issues Found
### 1. **CRITICAL** - Unhandled External API Call with Silent Failure
#### **File & Location**
- **File:** `src/controllers/ProfileEditController.ts`
- Lines 360-372: `newProfileEdit()` method
- **File:** `src/controllers/ProfileEditEmployeeController.ts`
- Lines 360-372: `profileEdit()` method
#### **Problem Type**
1. **Unhandled Exception**
2. **Silent Error Swallowing**
3. **Data Inconsistency Risk**
#### **Root Cause**
```typescript
// ProfileEditController.ts:360-372
await new CallAPI()
.PostData(req, "/org/workflow/add-workflow", {
refId: data.id,
sysName: "REGISTRY_PROFILE",
posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false,
orgRootId: orgRoot?.id ?? null
})
.catch((error) => {
console.error("Error calling API:", error);
});
// ❌ No re-throw, no proper error handling
```
**รายละเอียดปัญหา:**
1. **Silent Failure**: มีการใช้ `.catch()` แค่ log error แต่ไม่ throw หรือ handle error
2. **Data Inconsistency**: ข้อมูล ProfileEdit ถูกบันทึกแล้ว แต่ Workflow ไม่ได้ถูกสร้าง
3. **No Transaction**: ไม่มีการใช้ Transaction เพื่อ roll back ข้อมูลเมื่อ API ล้มเหลว
4. **User Confusion**: ผู้ใช้จะเห็นว่าบันทึกสำเร็จ แต่จริงๆ แล้ว Workflow ไม่ได้ทำงาน
**ผลกระทบ:**
- ข้อมูลใน Database ไม่สมบูรณ์ (ProfileEdit มีแต่ไม่มี Workflow)
- ผู้ใช้ไม่ทราบว่าเกิด Error จริงๆ
- ระบบอาจทำงานผิดปกติในภายหลังเมื่อมีการดำเนินการกับข้อมูลที่ไม่สมบูรณ์
---
### 2. **CRITICAL** - Potential Null Pointer Exception in Optional Chaining
#### **File & Location**
- **File:** `src/controllers/ProfileEditController.ts`
- Line 336-344: `newProfileEdit()` method
- **File:** `src/controllers/ProfileEditEmployeeController.ts`
- Line 337-345: `profileEdit()` method
#### **Problem Type**
1. **Unhandled Exception**
2. **TypeError Risk**
3. **Potential Crash**
#### **Root Cause**
```typescript
// ProfileEditController.ts:336-344
const orgRoot = await this.orgRootRepo.findOne({
select: {
id: true,
isDeputy: true
},
where: {
id: profile.current_holders.find(x => x.orgRootId)!.orgRootId ?? ""
// ^
// Non-null assertion without check
}
});
```
**รายละเอียดปัญหา:**
1. **Unsafe Array Access**: ใช้ `.find()` แล้วใช้ `!` (non-null assertion) โดยไม่มีการ check
2. **Potential TypeError**: หาก `.find()` return `undefined` การพยายามเข้าถึง `.orgRootId` จะทำให้เกิด `TypeError: Cannot read property 'orgRootId' of undefined`
3. **Unhandled Exception**: Error นี้จะทำให้ Service Crash ทันที
**สถานการณ์ที่อาจเกิดขึ้น:**
```typescript
// หาก current_holders เป็น empty array หรือไม่พบ element
profile.current_holders.find(x => x.orgRootId) // returns undefined
undefined!.orgRootId // ❌ CRASH: TypeError
```
---
### 3. **HIGH** - Unsafe Array Access in Multiple Locations
#### **File & Location**
- **File:** `src/controllers/ProfileEditController.ts`
- Line 278: `detailProfileEdit()` method
- **File:** `src/controllers/ProfileEditEmployeeController.ts`
- Line 277: `detailProfileEditEmp()` method
#### **Problem Type**
1. **Unhandled Exception**
2. **TypeError Risk**
#### **Root Cause**
```typescript
// ProfileEditController.ts:278-292
let orgRoot: OrgRoot | null = null;
if(getProfileEdit.profile) {
const empPosMaster = await this.posMasterRepo.findOne({
where: {
current_holderId: getProfileEdit.profile.id,
orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }
},
relations: { orgRevision: true }
});
if(empPosMaster) {
orgRoot = await this.orgRootRepo.findOne({
select: { isDeputy: true },
where: { id: empPosMaster.orgRootId ?? "" }
// ^^^^^^^^^^^^^^^^^^^
// May be null, using "" as fallback
});
}
}
```
**รายละเอียดปัญหา:**
1. **Unsafe Fallback**: ใช้ empty string `""` เป็น fallback สำหรับ `orgRootId`
2. **Silent Failure**: การ query ด้วย ID ว่างจะ return `null` แต่ไม่มีการแจ้งเตือน
3. **Data Integrity**: อาจทำให้ข้อมูล `isDeputy` ไม่ถูกต้อง
---
### 4. **HIGH** - Missing Error Handling in Database Update Operations
#### **File & Location**
- **File:** `src/controllers/ProfileDisciplineController.ts`
- Lines 167-172: `editDiscipline()` method
- **File:** `src/controllers/ProfileDisciplineEmployeeController.ts`
- Lines 172-177: `editDiscipline()` method
- **File:** `src/controllers/ProfileDisciplineEmployeeTempController.ts`
- Lines 162-167: `editDiscipline()` method
- **File:** `src/controllers/ProfileDutyController.ts`
- Lines 143-148: `editDuty()` method
- **File:** `src/controllers/ProfileDutyEmployeeController.ts`
- Lines 152-157: `editDuty()` method
- **File:** `src/controllers/ProfileDutyEmployeeTempController.ts`
- Lines 141-146: `editDuty()` method
#### **Problem Type**
1. **Missing Error Handle**
2. **Data Loss Risk**
#### **Root Cause**
```typescript
// Pattern found across multiple controllers
this.disciplineRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.disciplineHistoryRepository.save(history, { data: req });
// ❌ No await, no error handling
}
```
**รายละเอียดปัญหา:**
1. **Missing await**: ไม่มีการ `await` การ save history ทำให้ไม่รู้ว่า save สำเร็จหรือไม่
2. **No Error Handling**: หากการ save history ล้มเหลว จะไม่มีการ catch error
3. **Silent Failure**: History อาจไม่ถูกบันทึก แต่ไม่มีใครรู้
**ผลกระทบ:**
- History audit trail ไม่สมบูรณ์
- ไม่สามารถ trace back การเปลี่ยนแปลงได้
- การ audit และ debugging ยากขึ้น
---
### 5. **MEDIUM** - Complex Nested Query Without Error Handling
#### **File & Location**
- **File:** `src/controllers/ProfileEditController.ts`
- Lines 112-255: `detailProfileEditAdmin()` method
- **File:** `src/controllers/ProfileEditEmployeeController.ts`
- Lines 110-254: `detailProfileEditAdminEmp()` method
#### **Problem Type**
1. **Missing Error Handle**
2. **Performance Risk**
3. **Query Complexity Risk**
#### **Root Cause**
```typescript
// ProfileEditController.ts:122-193
const orgRevisionPublish = await this.orgRevisionRepository
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true")
.getOne(); // ❌ No null check, used in query below
let query = await AppDataSource.getRepository(ProfileEdit)
.createQueryBuilder("ProfileEdit")
.leftJoinAndSelect("ProfileEdit.profile", "profile")
.leftJoinAndSelect("profile.current_holders", "current_holders")
.leftJoinAndSelect("current_holders.orgRevision", "orgRevision")
.where((qb) => {
if (status != "" && status != null) {
qb.andWhere("ProfileEdit.status = :status", { status: status });
}
qb.andWhere("ProfileEdit.profileId IS NOT NULL");
})
.andWhere(orgRevisionPublish ? `current_holders.orgRevisionId = :revisionId` : "1=1", {
revisionId: orgRevisionPublish?.id, // ❌ Could be undefined
})
.andWhere(
data.root != undefined && data.root != null
? data.root[0] != null
? `current_holders.orgRootId IN (:...root)`
: `current_holders.orgRootId is null`
: "1=1",
{
root: data.root, // ❌ Could cause SQL error if undefined
},
)
// ... more complex conditions
```
**รายละเอียดปัญหา:**
1. **No Null Check**: `orgRevisionPublish` อาจเป็น `null` แต่ถูกใช้ใน query
2. **Complex Query Logic**: Query ที่ซับซ้อนมากหลายเงื่อนไข ไม่มีการ validate input
3. **SQL Injection Risk**: แม้จะใช้ Parameterized query แต่ยังมี dynamic SQL ที่อาจเสี่ยง
4. **No Timeout**: Query ขนาดใหญ่ไม่มี timeout อาจทำให้ connection hang
---
## Recommended Fixes
### Fix 1: Proper Error Handling for External API Calls
**Before:**
```typescript
await this.profileEditRepo.save(data);
await new CallAPI()
.PostData(req, "/org/workflow/add-workflow", {...})
.catch((error) => {
console.error("Error calling API:", error);
});
return new HttpSuccess(data.id);
```
**After:**
```typescript
// Option 1: Use Transaction Pattern
await AppDataSource.transaction(async (transactionalEntityManager) => {
// Save main data
const savedData = await transactionalEntityManager.save(ProfileEdit, data);
try {
// Call external API
await new CallAPI().PostData(req, "/org/workflow/add-workflow", {
refId: savedData.id,
sysName: "REGISTRY_PROFILE",
posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false,
orgRootId: orgRoot?.id ?? null
});
} catch (error) {
console.error("Failed to create workflow:", error);
// Rollback by throwing error
throw new HttpError(
HttpStatus.SERVICE_UNAVAILABLE,
"ไม่สามารถสร้าง Workflow ได้ กรุณาลองใหม่ภายหลัง"
);
}
});
return new HttpSuccess(data.id);
// Option 2: Async Pattern with Queue (Recommended for Production)
// Save data first, then process workflow asynchronously
const savedData = await this.profileEditRepo.save(data);
// Emit event for workflow creation
// await this.eventEmitter.emit('profile.edit.created', {
// profileEditId: savedData.id,
// profileId: profile.id,
// // ... other data
// });
return new HttpSuccess(savedData.id);
```
---
### Fix 2: Safe Array Access with Proper Null Checks
**Before:**
```typescript
const orgRoot = await this.orgRootRepo.findOne({
select: { id: true, isDeputy: true },
where: {
id: profile.current_holders.find(x => x.orgRootId)!.orgRootId ?? ""
}
});
```
**After:**
```typescript
// Safe access with proper null checks
const currentHolder = profile.current_holders?.find(x => x.orgRootId);
if (!currentHolder || !currentHolder.orgRootId) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"ไม่พบข้อมูลตำแหน่งปัจจุบัน กรุณาติดต่อ HR"
);
}
const orgRoot = await this.orgRootRepo.findOne({
select: { id: true, isDeputy: true },
where: { id: currentHolder.orgRootId }
});
if (!orgRoot) {
console.warn(`OrgRoot not found for id: ${currentHolder.orgRootId}`);
// Continue with default values or throw error based on business logic
}
```
---
### Fix 3: Add Proper Error Handling for Database Operations
**Before:**
```typescript
this.disciplineRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.disciplineHistoryRepository.save(history, { data: req });
}
```
**After:**
```typescript
try {
// Save main record
await this.disciplineRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
// Save history if needed
if (!(Object.keys(body).length === 1 && body.isUpload)) {
try {
await this.disciplineHistoryRepository.save(history, { data: req });
} catch (historyError) {
console.error("Failed to save history:", historyError);
// Log error but don't fail the request
// Consider using a message queue for audit logging
// await this.auditQueue.send({
// action: 'DISCIPLINE_UPDATE',
// data: history,
// error: historyError.message
// });
}
}
} catch (error) {
console.error("Failed to save discipline:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่"
);
}
```
---
### Fix 4: Add Query Timeout and Null Checks
**Before:**
```typescript
const orgRevisionPublish = await this.orgRevisionRepository
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true")
.getOne();
let query = await AppDataSource.getRepository(ProfileEdit)
.createQueryBuilder("ProfileEdit")
// ... complex query
```
**After:**
```typescript
// Add timeout and proper null handling
const orgRevisionPublish = await this.orgRevisionRepository
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true")
.setHint('maxExecutionTime', 5000) // 5 second timeout
.getOne();
// Validate permission data
if (!data || !data.root) {
throw new HttpError(
HttpStatus.FORBIDDEN,
"ไม่มีสิทธิ์เข้าถึงข้อมูล"
);
}
// Build query with validation
const queryBuilder = AppDataSource.getRepository(ProfileEdit)
.createQueryBuilder("ProfileEdit")
.leftJoinAndSelect("ProfileEdit.profile", "profile")
.leftJoinAndSelect("profile.current_holders", "current_holders")
.leftJoinAndSelect("current_holders.orgRevision", "orgRevision")
.where((qb) => {
if (status != "" && status != null) {
qb.andWhere("ProfileEdit.status = :status", { status: status });
}
qb.andWhere("ProfileEdit.profileId IS NOT NULL");
})
.setMaxResults(1000) // Prevent large result sets
.setHint('maxExecutionTime', 10000); // 10 second timeout
// Add revision filter only if valid
if (orgRevisionPublish?.id) {
queryBuilder.andWhere(
`current_holders.orgRevisionId = :revisionId`,
{ revisionId: orgRevisionPublish.id }
);
}
// Add root filter with validation
if (Array.isArray(data.root) && data.root.length > 0 && data.root[0] !== null) {
queryBuilder.andWhere(`current_holders.orgRootId IN (:...root)`, { root: data.root });
} else if (data.root?.[0] === null) {
queryBuilder.andWhere(`current_holders.orgRootId IS NULL`);
}
const [getProfileEdit, total] = await queryBuilder
.skip((page - 1) * pageSize)
.take(Math.min(pageSize, 100)) // Limit page size
.getManyAndCount();
```
---
### Fix 5: Implement Global Error Handler
**Create/Update `src/middlewares/error-handler.ts`:**
```typescript
import { Request, Response, NextFunction } from 'express';
import HttpError from '../interfaces/http-error';
export function globalErrorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
console.error('[Unhandled Exception]', {
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
body: req.body,
query: req.query
});
const isDevelopment = process.env.NODE_ENV === 'development';
if (err instanceof HttpError) {
return res.status(err.status).json({
error: err.message,
...(isDevelopment && { stack: err.stack })
});
}
// Handle TypeError from unsafe property access
if (err instanceof TypeError && err.message.includes("Cannot read")) {
return res.status(500).json({
error: 'Data access error',
...(isDevelopment && {
details: err.message,
stack: err.stack
})
});
}
// Generic error response
res.status(500).json({
error: 'Internal server error',
...(isDevelopment && {
message: err.message,
stack: err.stack
})
});
}
// Handle unhandled promise rejections
export function setupUnhandledRejectionHandler() {
process.on('unhandledRejection', (reason, promise) => {
console.error('[Unhandled Rejection] at:', promise, 'reason:', reason);
// Send to monitoring service
// monitoringService.captureException(reason);
});
process.on('uncaughtException', (error) => {
console.error('[Uncaught Exception]', error);
// Send to monitoring service
// monitoringService.captureException(error);
// Graceful shutdown
cleanup();
process.exit(1);
});
}
async function cleanup() {
// Close database connections
await AppDataSource.destroy();
// Close other resources
}
```
---
## Summary Statistics
| Issue Type | Count | Severity |
|------------|-------|----------|
| Unhandled External API Call (Silent Failure) | 2 | CRITICAL |
| Unsafe Array Access (Null Pointer Risk) | 2 | CRITICAL |
| Missing Error Handling in DB Operations | 12 | HIGH |
| Complex Query Without Timeout/Null Check | 2 | MEDIUM |
| Data Inconsistency Risk | 4 | HIGH |
---
## Files Requiring Immediate Attention
1. ✅ `src/controllers/ProfileEditController.ts` - CRITICAL (Line 336, 360)
2. ✅ `src/controllers/ProfileEditEmployeeController.ts` - CRITICAL (Line 337, 360)
3. ✅ `src/controllers/ProfileDisciplineController.ts` - HIGH (Line 167)
4. ✅ `src/controllers/ProfileDisciplineEmployeeController.ts` - HIGH (Line 172)
5. ✅ `src/controllers/ProfileDisciplineEmployeeTempController.ts` - HIGH (Line 162)
6. ✅ `src/controllers/ProfileDutyController.ts` - HIGH (Line 143)
7. ✅ `src/controllers/ProfileDutyEmployeeController.ts` - HIGH (Line 152)
8. ✅ `src/controllers/ProfileDutyEmployeeTempController.ts` - HIGH (Line 141)
---
## Priority Recommendations
### P0 (Immediate Action Required)
1. Fix unsafe array access with non-null assertion (`!`)
2. Add proper error handling for external API calls (CallAPI)
3. Implement transaction pattern for multi-step operations
### P1 (This Sprint)
4. Add error handling for all database save operations
5. Implement query timeout for complex queries
6. Add input validation for query parameters
### P2 (Next Sprint)
7. Implement async event queue for external API calls
8. Add comprehensive monitoring and alerting
9. Implement circuit breaker pattern for external services
---
## Testing Recommendations
1. **Unit Tests**: Test null/undefined scenarios for array access
2. **Integration Tests**: Test external API failure scenarios
3. **Load Tests**: Test query performance with large datasets
4. **Chaos Testing**: Test behavior when external services are down
5. **Data Consistency Tests**: Verify transaction rollback behavior
---
**Report Generated:** 2026-05-08
**Batch:** 09 (Controllers 81-90)
**Total Files Analyzed:** 10
**Critical Issues Found:** 5
**High Priority Issues:** 14

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,442 +0,0 @@
# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop
## Batch 12: Controllers 111-120
**วันที่ตรวจสอบ:** 2026-05-08
**จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers
---
## Controllers ที่ตรวจสอบในชุดนี้
1. [ProfileInsigniaController.ts](src/controllers/ProfileInsigniaController.ts)
2. [ProfileInsigniaEmployeeController.ts](src/controllers/ProfileInsigniaEmployeeController.ts)
3. [ProfileInsigniaEmployeeTempController.ts](src/controllers/ProfileInsigniaEmployeeTempController.ts)
4. [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts)
5. [ProfileLeaveEmployeeController.ts](src/controllers/ProfileLeaveEmployeeController.ts)
6. [ProfileLeaveEmployeeTempController.ts](src/controllers/ProfileLeaveEmployeeTempController.ts)
7. [ProfileNopaidController.ts](src/controllers/ProfileNopaidController.ts)
8. [ProfileNopaidEmployeeController.ts](src/controllers/ProfileNopaidEmployeeController.ts)
9. [ProfileNopaidEmployeeTempController.ts](src/controllers/ProfileNopaidEmployeeTempController.ts)
10. [ProfileOtherController.ts](src/controllers/ProfileOtherController.ts)
---
## รายการปัญหาที่พบ
### 1. 🔴 CRITICAL - ProfileInsigniaController.ts - Unhandled Promise in editInsignia
**File & Location:** [ProfileInsigniaController.ts](src/controllers/ProfileInsigniaController.ts:192-197) - `editInsignia()` method
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
```typescript
this.insigniaRepo.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.insigniaHistoryRepo.save(history, { data: req });
}
```
- มีการเรียก `this.insigniaRepo.save()` และ `this.insigniaHistoryRepo.save()` โดยไม่มี `await` หรือการจัดการ error
- ถ้าเกิด error จากการ save database จะทำให้เกิด **Unhandled Promise Rejection**
- ไม่มี try-catch รองรับ
**Recommended Fix:**
```typescript
try {
await this.insigniaRepo.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
await this.insigniaHistoryRepo.save(history, { data: req });
}
return new HttpSuccess();
} catch (error) {
console.error('Error updating insignia:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์'
);
}
```
---
### 2. 🔴 CRITICAL - ProfileInsigniaEmployeeController.ts - Unhandled Promise in editInsignia
**File & Location:** [ProfileInsigniaEmployeeController.ts](src/controllers/ProfileInsigniaEmployeeController.ts:200-205) - `editInsignia()` method
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
```typescript
this.insigniaRepo.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.insigniaHistoryRepo.save(history, { data: req });
}
```
- มีการเรียก `this.insigniaRepo.save()` และ `this.insigniaHistoryRepo.save()` โดยไม่มี `await`
- ถ้า database save ล้มเหลวจะเกิด **Unhandled Promise Rejection**
- Data inconsistency อาจเกิดขึ้นถ้า history save ไม่สำเร็จ
**Recommended Fix:**
```typescript
try {
await this.insigniaRepo.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
await this.insigniaHistoryRepo.save(history, { data: req });
}
return new HttpSuccess();
} catch (error) {
console.error('Error updating employee insignia:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์'
);
}
```
---
### 3. 🔴 CRITICAL - ProfileInsigniaEmployeeTempController.ts - Unhandled Promise in editInsignia
**File & Location:** [ProfileInsigniaEmployeeTempController.ts](src/controllers/ProfileInsigniaEmployeeTempController.ts:189-194) - `editInsignia()` method
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
```typescript
this.insigniaRepo.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.insigniaHistoryRepo.save(history, { data: req });
}
```
- ไม่มีการ await หรือจัดการ error สำหรับ database operations
- ถ้าเกิด error จะทำให้เกิด **Unhandled Promise Rejection** และอาจ crash service
**Recommended Fix:**
```typescript
try {
await this.insigniaRepo.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
await this.insigniaHistoryRepo.save(history, { data: req });
}
return new HttpSuccess();
} catch (error) {
console.error('Error updating temp employee insignia:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์'
);
}
```
---
### 4. 🔴 CRITICAL - ProfileLeaveController.ts - Unhandled Promise in editLeave
**File & Location:** [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts:312) - `updateCancel()` method
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
```typescript
@Patch("cancel/{leaveId}")
public async updateCancel(
@Request() req: RequestWithUser,
@Path() leaveId: string,
) {
const record = await this.leaveRepo.findOneBy({ leaveId: leaveId }); // ❌ ใช้ leaveId แทน id
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
// ...
```
- **BUG**: ใช้ `leaveId` ใน `findOneBy({ leaveId: leaveId })` แต่ column ที่ถูกต้องควรเป็น `id`
- ถ้าไม่พบข้อมูลจะ throw HttpError แต่ถ้า database error จะเกิด unhandled exception
- ไม่มี try-catch ครอบ database operations
**Recommended Fix:**
```typescript
@Patch("cancel/{leaveId}")
public async updateCancel(
@Request() req: RequestWithUser,
@Path() leaveId: string,
) {
try {
const record = await this.leaveRepo.findOneBy({ id: leaveId }); // ✅ ใช้ id แทน leaveId
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
const before = structuredClone(record);
record.status = "cancel";
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
record.lastUpdatedAt = new Date();
await Promise.all([
this.leaveRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
]);
return new HttpSuccess();
} catch (error) {
if (error instanceof HttpError) throw error;
console.error('Error canceling leave:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการยกเลิกการลา'
);
}
}
```
---
### 5. 🔴 CRITICAL - ProfileLeaveEmployeeTempController.ts - Unhandled Promises
**File & Location:** [ProfileLeaveEmployeeTempController.ts](src/controllers/ProfileLeaveEmployeeTempController.ts:132-134) - `newLeave()` method
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
```typescript
await this.leaveRepo.save(data); // ❌ ไม่มี { data: req } context
history.profileLeaveId = data.id; // ❌ ใช้ data.id ที่อาจยังไม่ถูกต้องถ้า save ไม่สำเร็จ
await this.leaveHistoryRepo.save(history); // ❌ ไม่มี { data: req } context
```
- ไม่มี error handling รอบ database operations
- การไม่ใส่ `{ data: req }` อาจทำให้ audit trail ไม่สมบูรณ์
- ถ้า `leaveRepo.save()` ล้มเหลว จะเกิด unhandled rejection
**Recommended Fix:**
```typescript
try {
await this.leaveRepo.save(data, { data: req });
setLogDataDiff(req, { before, after: data });
history.profileLeaveId = data.id;
await this.leaveHistoryRepo.save(history, { data: req });
return new HttpSuccess(data.id);
} catch (error) {
console.error('Error creating employee temp leave:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการบันทึกข้อมูลการลา'
);
}
```
---
### 6. 🟡 HIGH - ProfileNopaidController.ts - Unhandled Promise in editNopaid
**File & Location:** [ProfileNopaidController.ts](src/controllers/ProfileNopaidController.ts:133-137) - `editNopaid()` method
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
```typescript
this.nopaidRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.nopaidHistoryRepository.save(history, { data: req });
}
```
- ไม่มี `await` สำหรับ database save operations
- ถ้าเกิด error จะเป็น **Unhandled Promise Rejection**
- ไม่มี try-catch ครอบ
**Recommended Fix:**
```typescript
try {
await this.nopaidRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
await this.nopaidHistoryRepository.save(history, { data: req });
}
return new HttpSuccess();
} catch (error) {
console.error('Error updating nopaid:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน'
);
}
```
---
### 7. 🟡 HIGH - ProfileNopaidEmployeeController.ts - Unhandled Promise in editNopaid
**File & Location:** [ProfileNopaidEmployeeController.ts](src/controllers/ProfileNopaidEmployeeController.ts:140-144) - `editNopaid()` method
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
```typescript
this.nopaidRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.nopaidHistoryRepository.save(history, { data: req });
}
```
- ไม่มีการ await database save operations
- ถ้าเกิด error จะทำให้เกิด unhandled promise rejection
**Recommended Fix:**
```typescript
try {
await this.nopaidRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
await this.nopaidHistoryRepository.save(history, { data: req });
}
return new HttpSuccess();
} catch (error) {
console.error('Error updating employee nopaid:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน'
);
}
```
---
### 8. 🟡 HIGH - ProfileNopaidEmployeeTempController.ts - Unhandled Promise in editNopaid
**File & Location:** [ProfileNopaidEmployeeTempController.ts](src/controllers/ProfileNopaidEmployeeTempController.ts:137-141) - `editNopaid()` method
**Problem Type:** 1. Unhandled Exception
**Root Cause:**
```typescript
this.nopaidRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
this.nopaidHistoryRepository.save(history, { data: req });
}
```
- ไม่มี `await` สำหรับ database operations
- Unhandled promise rejection อาจเกิดขึ้น
**Recommended Fix:**
```typescript
try {
await this.nopaidRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
await this.nopaidHistoryRepository.save(history, { data: req });
}
return new HttpSuccess();
} catch (error) {
console.error('Error updating temp employee nopaid:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน'
);
}
```
---
### 9. 🟢 MEDIUM - ProfileLeaveController.ts - Missing Permission Check in updateCancel
**File & Location:** [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts:308-328) - `updateCancel()` method
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
```typescript
@Patch("cancel/{leaveId}")
public async updateCancel(
@Request() req: RequestWithUser,
@Path() leaveId: string,
) {
const record = await this.leaveRepo.findOneBy({ leaveId: leaveId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
const before = structuredClone(record);
record.status = "cancel";
// ... ❌ ไม่มี permission check
```
- Method `updateCancel` ไม่มีการ check permission ก่อนทำการ cancel
- ผู้ใช้ที่ไม่มีสิทธิ์อาจสามารถ cancel การลาของคนอื่นได้
- เมื่อเทียบกับ methods อื่นๆ ที่มี permission check ถือว่าเป็นความไม่สอดคล้อง
**Recommended Fix:**
```typescript
@Patch("cancel/{leaveId}")
public async updateCancel(
@Request() req: RequestWithUser,
@Path() leaveId: string,
) {
try {
const record = await this.leaveRepo.findOneBy({ id: leaveId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
// ✅ เพิ่ม permission check
await new permission().PermissionOrgUserUpdate(
req,
"SYS_REGISTRY_OFFICER",
record.profileId
);
const before = structuredClone(record);
record.status = "cancel";
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
record.lastUpdatedAt = new Date();
await Promise.all([
this.leaveRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
]);
return new HttpSuccess();
} catch (error) {
if (error instanceof HttpError) throw error;
console.error('Error canceling leave:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'เกิดข้อผิดพลาดในการยกเลิกการลา'
);
}
}
```
---
## สรุปประเด็นสำคัญ
### ปัญหาที่พบเป็นพื้นฐานซ้ำๆ:
1. **Unhandled Promise Rejections** - การเรียก database save methods โดยไม่มี `await` ใน methods แก้ไขข้อมูล (edit/update)
2. **Missing Try-Catch Blocks** - การขาด error handling รอบ database operations
3. **Data Consistency Risks** - การบันทึก history โดยไม่รู้ว่า main record บันทึกสำเร็จหรือไม่
4. **Bug in updateCancel** - การใช้ `leaveId` แทน `id` ใน findOneBy
### คำแนะนำในการแก้ไข:
1. เพิ่ม try-catch ครอบทุก database operations ที่เสี่ยงต่อการเกิด error
2. ใช้ `await` กับทุก promise ที่เกี่ยวกับ database save/update
3. เพิ่ม permission check ใน method `updateCancel`
4. แก้ไข bug การใช้ `leaveId` ใน findOneBy ให้เป็น `id`
5. พิจารณาใช้ Transaction สำหรับการบันทึกข้อมูลที่ต้องการความสอดคล้องกัน (main record + history)
### การประเมินความเสี่ยง:
- 🔴 **CRITICAL**: 4 จุด - อาจทำให้เกิด Unhandled Exception และ Crash Loop
- 🟡 **HIGH**: 4 จุด - อาจทำให้เกิด Unhandled Exception
- 🟢 **MEDIUM**: 1 จุด - ปัญหาความปลอดภัยและความสอดคล้องของระบบ

View file

@ -1,844 +0,0 @@
# Batch 13 Controllers Analysis (Controllers 121-130)
## Controllers in this batch:
1. ProfileOtherEmployeeController
2. ProfileOtherEmployeeTempController
3. ProfileSalaryController
4. ProfileSalaryEmployeeController
5. ProfileSalaryEmployeeTempController
6. ProfileSalaryTempController
7. ProfileTrainingController
8. ProfileTrainingEmployeeController
9. ProfileTrainingEmployeeTempController
10. ProvinceController
---
## Critical Issues Found
### 1. **ProfileSalaryTempController** - Multiple Unhandled forEach Async Operations
**File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts) - Methods: `listSalary()`, `confirmDoneSalary()`, `changeSortEditGenAll()`, `changeSortEdit()`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
Multiple methods use `forEach()` with async operations without proper error handling or awaiting. When errors occur in these async callbacks, they become unhandled rejections that can crash the Node.js process.
**Affected Code Locations:**
- Line 1058-1061: `salaryOld.forEach(async (p, i) => { ... })` in `deleteSalary()`
- Line 1115-1118: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalary()`
- Line 202-205: `salaryOld.forEach((item: any, i) => { ... })` in `listSalary()` (sync operations but no error handling)
- Line 1729-1741: `for await` loop with database operations without error handling in `changeSortEditGenAll()`
- Line 1763-1766: `salaryOld.forEach()` in `changeSortEdit()`
**Code Examples:**
```typescript
// Line 1115-1118 - DANGEROUS: async forEach without error handling
salaryList.forEach(async (p, i) => {
p.order = i + 1;
await this.salaryRepo.save(p); // If this fails, error is unhandled
});
```
```typescript
// Line 1729-1741 - DANGEROUS: for await without try-catch
for await (const item of profiles) {
let salaryOld = await this.salaryOldRepo.find({
where: { profileId: item.id },
order: { commandDateAffect: "ASC", order: "ASC" },
});
salaryOld.forEach((item: any, i) => {
item.order = i + 1;
});
num = num + 1;
console.log(num);
await this.salaryOldRepo.save(salaryOld); // If this fails, entire operation crashes
}
```
**Recommended Fix:**
```typescript
// For deleteSalary() - Use Promise.all with error handling
try {
await Promise.all(
salaryList.map(async (p, i) => {
p.order = i + 1;
await this.salaryRepo.save(p);
})
);
} catch (error) {
console.error('Error updating salary order:', error);
// Optionally throw a more specific error or handle gracefully
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to update salary order');
}
// For changeSortEditGenAll() - Add error handling per iteration
try {
const profiles = await this.profileRepo.find();
let num = 1;
for await (const item of profiles) {
try {
let salaryOld = await this.salaryOldRepo.find({
where: { profileId: item.id },
order: { commandDateAffect: "ASC", order: "ASC" },
});
salaryOld.forEach((item: any, i) => {
item.order = i + 1;
});
await this.salaryOldRepo.save(salaryOld);
num = num + 1;
console.log(num);
} catch (error) {
console.error(`Error processing profile ${item.id}:`, error);
// Continue with next profile instead of crashing
continue;
}
}
return new HttpSuccess();
} catch (error) {
console.error('Error in changeSortEditGenAll:', error);
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to process profiles');
}
```
---
### 2. **ProfileSalaryController** - Unhandled forEach Async Operations
**File & Location:** [ProfileSalaryController.ts](src/controllers/ProfileSalaryController.ts) - Methods: `deleteSalary()`, `Registry()`, `RegistryEmployee()`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
Multiple critical methods use `forEach()` with async database operations. When database operations fail within these callbacks, the Promise rejection is unhandled and can crash the service.
**Affected Code Locations:**
- Line 1115-1118: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalary()`
- Line 362-373: Complex async operations in `Registry()` without error handling
- Line 383-395: Complex async operations in `RegistryEmployee()` without error handling
- Line 412-427: `record.map(async (r) => { ... })` with `Promise.all()` but no error handling
- Line 463-477: Similar pattern in `getSalaryPositionUser()`
- Line 497-512: Similar pattern in `getSalary()`
**Code Examples:**
```typescript
// Line 1115-1118 - CRITICAL: async forEach without error handling
salaryList.forEach(async (p, i) => {
p.order = i + 1;
await this.salaryRepo.save(p); // Unhandled rejection
});
```
```typescript
// Line 412-427 - Promise.all without error handling
const result = await Promise.all(
record.map(async (r) => {
let _command = null;
if (r.commandId) {
_command = await this.commandRepository.findOne({
where: { id: r.commandId },
relations: ["commandType"],
});
}
return {
...r,
commandType: _command && _command?.commandType ? _command?.commandType.code : null,
};
}),
);
```
**Recommended Fix:**
```typescript
// For deleteSalary() - Proper error handling
try {
await Promise.all(
salaryList.map(async (p, i) => {
p.order = i + 1;
await this.salaryRepo.save(p);
})
);
} catch (error) {
console.error('Error updating salary order:', error);
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to update salary order');
}
// For Promise.all operations - Add error boundary
try {
const result = await Promise.all(
record.map(async (r) => {
try {
let _command = null;
if (r.commandId) {
_command = await this.commandRepository.findOne({
where: { id: r.commandId },
relations: ["commandType"],
});
}
return {
...r,
commandType: _command && _command?.commandType ? _command?.commandType.code : null,
};
} catch (error) {
console.error(`Error loading command for salary ${r.id}:`, error);
return {
...r,
commandType: null,
};
}
}),
);
return new HttpSuccess(result);
} catch (error) {
console.error('Error processing salary records:', error);
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to load salary data');
}
// For Registry() - Add comprehensive error handling
try {
await this.registryRepo.clear();
const allRegis = await AppDataSource.getRepository(viewRegistryOfficer)
.createQueryBuilder("registryOfficer")
.getMany();
const profileIds = new Set((await this.profileRepo.find()).map((p) => p.id));
const mapData = allRegis
.filter((x) => profileIds.has(x.profileId))
.map((x) => ({
...x,
isProbation: Boolean(x.isProbation),
isLeave: Boolean(x.isLeave),
isRetirement: Boolean(x.isRetirement),
Educations: x.Educations ? JSON.stringify(x.Educations) : "",
}));
if (mapData.length > 0) {
// Save in batches to avoid overwhelming the database
const batchSize = 100;
for (let i = 0; i < mapData.length; i += batchSize) {
const batch = mapData.slice(i, i + batchSize);
await this.registryRepo.save(batch);
}
}
return new HttpSuccess();
} catch (error) {
console.error('Error in Registry cronjob:', error);
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to sync registry data');
}
```
---
### 3. **ProfileSalaryController** - Raw SQL Queries Without Error Handling
**File & Location:** [ProfileSalaryController.ts](src/controllers/ProfileSalaryController.ts) - Multiple methods using `AppDataSource.query()`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
Multiple stored procedure calls (`CALL GetProfile...()`) are executed without try-catch blocks. If these stored procedures fail or the database is unavailable, the errors will propagate unhandled.
**Affected Code Locations:**
- Line 76-79: `CALL GetProfileSalaryPosition(?, ?)` in `cronjobTenurePositionOfficer()`
- Line 126-129: Similar in `cronjobTenurePositionEmployee()`
- Line 176-179: `CALL GetProfileSalaryLevel(?, ?)` in `cronjobTenureLevelOfficer()`
- Line 236-239: Similar in `cronjobTenureLevelEmployee()`
- Line 317-320: `CALL GetProfileSalaryExecutive(?, ?)` in `cronjobTenureExecutivePositionOfficer()`
- Line 588-591, 622-625, 662-665: Multiple calls in `getPositionTenureUser()`
- Line 722-725, 760-763, 803-806: Multiple calls in `getPositionTenure()`
**Code Examples:**
```typescript
// Line 76-79 - No error handling for stored procedure call
const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [
x.id,
_currentDate,
]);
```
**Recommended Fix:**
```typescript
// Wrap all database query calls in try-catch
try {
const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [
x.id,
_currentDate,
]);
const _position = position.length > 0 ? position[0] : [];
const mapPosition =
_position.length > 1
? _position.slice(1).map((curr: any, index: number) => ({
days_diff: curr.days_diff,
positionName: _position[index]?.positionName,
}))
: [];
const calDayDiff = mapPosition
.filter((curr: any) => curr.positionName == x.position)
.reduce(
(acc: any, curr: any) => {
acc.days_diff += Number(curr.days_diff) || 0;
acc.positionName = curr.positionName;
return acc;
},
{ days_diff: 0, positionName: null },
);
const { year, month, day } = calculateTenure(calDayDiff.days_diff);
const mapData: any = {
profileId: x.id,
positionName: calDayDiff.positionName,
days_diff: calDayDiff.days_diff,
Years: year,
Months: month,
Days: day,
};
data.push(mapData);
} catch (error) {
console.error(`Error processing position tenure for profile ${x.id}:`, error);
// Add default/error entry or skip this profile
const mapData: any = {
profileId: x.id,
positionName: null,
days_diff: 0,
Years: 0,
Months: 0,
Days: 0,
error: true,
};
data.push(mapData);
}
```
---
### 4. **ProfileTrainingController** - Multiple Database Operations Without Error Handling
**File & Location:** [ProfileTrainingController.ts](src/controllers/ProfileTrainingController.ts) - Methods: `deleteAllTraining()`, `deleteById()`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
Multiple sequential delete operations without transaction or error handling. If intermediate operations fail, the database can be left in an inconsistent state.
**Affected Code Locations:**
- Line 238-259: `deleteAllTraining()` - Multiple delete operations without transaction
- Line 274-339: `deleteById()` - Multiple delete operations without transaction
**Code Examples:**
```typescript
// Line 238-259 - No error handling or transaction
const trainings = await this.trainingRepo.find({
select: { id: true },
where: { developmentId: reqBody.developmentId },
});
if (trainings.length > 0) {
const trainingIds = trainings.map((x) => x.id);
await this.trainingHistoryRepo.delete({
profileTrainingId: In(trainingIds),
});
await this.trainingRepo.delete({
developmentId: reqBody.developmentId,
});
}
await this.developmentHistoryRepo.delete({
kpiDevelopmentId: reqBody.developmentId,
});
await this.developmentRepo.delete({
kpiDevelopmentId: reqBody.developmentId
});
```
**Recommended Fix:**
```typescript
@Post("delete-all")
public async deleteAllTraining(
@Body() reqBody: { developmentId: string },
@Request() req: RequestWithUser
) {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const trainings = await queryRunner.manager.find(ProfileTraining, {
select: { id: true },
where: { developmentId: reqBody.developmentId },
});
if (trainings.length > 0) {
const trainingIds = trainings.map((x) => x.id);
await queryRunner.manager.delete(ProfileTrainingHistory, {
profileTrainingId: In(trainingIds),
});
await queryRunner.manager.delete(ProfileTraining, {
developmentId: reqBody.developmentId,
});
}
await queryRunner.manager.delete(ProfileDevelopmentHistory, {
kpiDevelopmentId: reqBody.developmentId,
});
await queryRunner.manager.delete(ProfileDevelopment, {
kpiDevelopmentId: reqBody.developmentId
});
await queryRunner.commitTransaction();
return new HttpSuccess();
} catch (error) {
await queryRunner.rollbackTransaction();
console.error('Error deleting training data:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'Failed to delete training data'
);
} finally {
await queryRunner.release();
}
}
// Similar fix for deleteById()
@Post("delete-byId")
public async deleteById(
@Body() reqBody: {
type: string;
profileId: string;
developmentId: string;
},
@Request() req: RequestWithUser
) {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const type = reqBody.type?.trim().toUpperCase();
// 1. validate profile
if (type === "OFFICER") {
const profile = await queryRunner.manager.findOne(Profile, {
where: { id: reqBody.profileId }
});
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
} else {
const profile = await queryRunner.manager.findOne(ProfileEmployee, {
where: { id: reqBody.profileId }
});
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
}
const profileField = type === "OFFICER" ? "profileId" : "profileEmployeeId";
// 2. Find and delete ProfileTraining
const trainings = await queryRunner.manager.find(ProfileTraining, {
select: { id: true },
where: {
developmentId: reqBody.developmentId,
[profileField]: reqBody.profileId,
},
});
if (trainings.length > 0) {
const trainingIds = trainings.map(x => x.id);
await queryRunner.manager.delete(ProfileTrainingHistory, {
profileTrainingId: In(trainingIds),
});
await queryRunner.manager.delete(ProfileTraining, {
id: In(trainingIds),
});
}
// 3. Find and delete ProfileDevelopment
const developments = await queryRunner.manager.find(ProfileDevelopment, {
select: { id: true },
where: {
kpiDevelopmentId: reqBody.developmentId,
[profileField]: reqBody.profileId,
},
});
if (developments.length > 0) {
const devIds = developments.map(x => x.id);
await queryRunner.manager.delete(ProfileDevelopmentHistory, {
profileDevelopmentId: In(devIds),
});
await queryRunner.manager.delete(ProfileDevelopment, {
id: In(devIds),
});
}
await queryRunner.commitTransaction();
return new HttpSuccess();
} catch (error) {
await queryRunner.rollbackTransaction();
console.error('Error deleting by ID:', error);
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'Failed to delete data'
);
} finally {
await queryRunner.release();
}
}
```
---
### 5. **ProfileSalaryEmployeeController** - forEach Async Operations Without Error Handling
**File & Location:** [ProfileSalaryEmployeeController.ts](src/controllers/ProfileSalaryEmployeeController.ts) - Method: `deleteSalaryEmployee()`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
Similar to ProfileSalaryController, uses `forEach()` with async operations without proper error handling.
**Affected Code Locations:**
- Line 608-611: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalaryEmployee()`
**Code Example:**
```typescript
// Line 608-611 - DANGEROUS
salaryList.forEach(async (p, i) => {
p.order = i + 1;
await this.salaryRepo.save(p); // Unhandled rejection
});
```
**Recommended Fix:**
```typescript
try {
await Promise.all(
salaryList.map(async (p, i) => {
p.order = i + 1;
await this.salaryRepo.save(p);
})
);
} catch (error) {
console.error('Error updating salary order:', error);
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to update salary order');
}
```
---
### 6. **ProfileSalaryEmployeeTempController** - forEach Async Operations Without Error Handling
**File & Location:** [ProfileSalaryEmployeeTempController.ts](src/controllers/ProfileSalaryEmployeeTempController.ts) - Method: `deleteSalaryEmployee()`
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
**Root Cause:**
Same pattern as above - `forEach()` with async operations.
**Affected Code Locations:**
- Line 202-205: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalaryEmployee()`
**Recommended Fix:** Same as above - use `Promise.all()` with error handling.
---
### 7. **ProfileSalaryTempController** - confirmDoneSalary() Transaction Handling Issues
**File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts) - Method: `confirmDoneSalary()`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
While this method uses transactions, there are several potential issues:
1. Line 1686: Empty `catch` block that swallows all errors
2. Line 1493-1497: Error is re-thrown without proper logging or context
3. Multiple complex operations within transaction that could fail
**Affected Code Locations:**
- Line 1493-1498: `catch` block re-throws error without logging
- Line 1685: Empty `catch` block in `returnEdit()`
**Code Examples:**
```typescript
// Line 1493-1498 - Insufficient error handling
} catch (error) {
await queryRunner.rollbackTransaction();
throw error; // No logging, no context
} finally {
await queryRunner.release();
}
```
**Recommended Fix:**
```typescript
} catch (error) {
await queryRunner.rollbackTransaction();
console.error('Error in confirmDoneSalary:', {
profileId: body.profileId,
type: body.type,
error: error instanceof Error ? error.message : error,
stack: error instanceof Error ? error.stack : undefined,
});
// Provide more specific error message
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'Failed to confirm salary data. Please try again.'
);
} finally {
await queryRunner.release();
}
// For returnEdit() - Proper error handling
try {
if (profile) {
profile.statusCheckEdit = "PENDING";
await this.profileRepo.save(profile);
} else if (profileEmployee) {
profileEmployee.statusCheckEdit = "PENDING";
await this.profileEmployeeRepo.save(profileEmployee);
}
const history: PositionSalaryEditHistory = Object.assign(
new PositionSalaryEditHistory(),
body,
);
if (profile) {
history.profileId = profileId;
} else if (profileEmployee) {
history.profileEmployeeId = profileId;
}
history.returnedDate = new Date();
history.examinerName = req.user.name;
history.createdFullName = req.user.name;
history.lastUpdateFullName = req.user.name;
await this.positionSalaryEditHistoryRepo.save(history);
return new HttpSuccess();
} catch (error) {
console.error('Error in returnEdit:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
'Failed to process return edit request'
);
}
```
---
### 8. **ProfileSalaryTempController** - Bulk Operations Without Error Handling
**File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts) - Methods: `listSalary()`, `confirmDoneSalary()`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
Bulk insert operations without error handling for individual records. If one record fails, the entire operation may fail or data may be partially inserted.
**Affected Code Locations:**
- Line 1058-1061: `salaryOld.forEach()` without error handling
- Line 1098-1101: Similar pattern
- Line 1425-1431: Bulk insert without error handling
**Code Example:**
```typescript
// Line 1425-1431 - Bulk insert without error handling
if (salaryRows.length) {
await queryRunner.manager.insert(
ProfileSalary,
salaryRows.map(({ id, ...data }) => ({
...data,
...metaCreated,
})),
);
}
```
**Recommended Fix:**
```typescript
// Implement batch processing with error handling
if (salaryRows.length) {
const batchSize = 100; // Process in batches
for (let i = 0; i < salaryRows.length; i += batchSize) {
const batch = salaryRows.slice(i, i + batchSize);
try {
await queryRunner.manager.insert(
ProfileSalary,
batch.map(({ id, ...data }) => ({
...data,
...metaCreated,
}))
);
} catch (error) {
console.error(`Error inserting salary batch ${i / batchSize + 1}:`, error);
// Log which records failed
const failedIds = batch.map(b => b.id);
console.error('Failed record IDs:', failedIds);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
`Failed to insert salary records (batch ${i / batchSize + 1})`
);
}
}
}
```
---
### 9. **ProvinceController** - Try-Catch With Generic Error Handling
**File & Location:** [ProvinceController.ts](src/controllers/ProvinceController.ts) - Method: `Delete()`
**Problem Type:** 2. Missing Error Handle
**Root Cause:**
While there is a try-catch block, it catches all errors without logging or differentiation. This makes debugging difficult and may mask underlying issues.
**Affected Code Locations:**
- Line 168-175: Generic catch block
**Code Example:**
```typescript
// Line 168-175 - Generic error handling
let result: any;
try {
result = await this.provinceRepository.delete({ id: id });
} catch {
throw new HttpError(
HttpStatusCode.NOT_FOUND,
"ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลจังหวัดนี้อยู่",
);
}
```
**Recommended Fix:**
```typescript
let result: any;
try {
result = await this.provinceRepository.delete({ id: id });
} catch (error) {
console.error('Error deleting province:', {
id,
error: error instanceof Error ? error.message : error,
stack: error instanceof Error ? error.stack : undefined,
});
// Check for foreign key constraint error
if (error instanceof Error && error.message.includes('foreign key constraint')) {
throw new HttpError(
HttpStatusCode.CONFLICT, // Use 409 instead of 404
"ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลจังหวัดนี้อยู่",
);
}
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาดในการลบข้อมูลจังหวัด",
);
}
```
---
## Summary Statistics
**Total Critical Issues Found:** 9
**Breakdown by Type:**
- **Unhandled Exception (forEach with async):** 6 instances
- **Missing Error Handling (DB operations):** 8 instances
- **Transaction Issues:** 2 instances
- **Generic Error Handling:** 1 instance
**Controllers with Issues:**
1. ProfileSalaryTempController - 4 critical issues
2. ProfileSalaryController - 3 critical issues
3. ProfileSalaryEmployeeController - 1 critical issue
4. ProfileSalaryEmployeeTempController - 1 critical issue
5. ProfileTrainingController - 2 critical issues
6. ProvinceController - 1 minor issue
**Risk Level: HIGH**
---
## Priority Recommendations
### Immediate Actions Required:
1. **Replace all `forEach()` with async operations** - Use `Promise.all()` or `for...of` loops with proper error handling
2. **Add error boundaries** around all database operations
3. **Implement proper logging** for all errors
4. **Use transactions** for multi-step database operations
5. **Add circuit breakers** for external dependencies (database, stored procedures)
### Graceful Recovery Strategies:
1. **Implement request-level error boundaries** - Catch errors at the controller level and return appropriate HTTP responses
2. **Add database operation timeouts** - Prevent indefinite hangs
3. **Implement retry logic** for transient database errors
4. **Add health checks** - Monitor database connectivity
5. **Use connection pooling** with proper error handling
### Long-term Improvements:
1. **Implement a centralized error handling middleware**
2. **Add structured logging** (e.g., Winston, Pino)
3. **Implement request tracing** for debugging
4. **Add metrics/monitoring** for error rates
5. **Implement graceful shutdown** procedures
---
## Testing Recommendations
1. **Test database failure scenarios** - Disconnect database during operations
2. **Test with large datasets** - Ensure forEach operations don't cause memory issues
3. **Test transaction rollback** - Verify data consistency on errors
4. **Test concurrent requests** - Ensure race conditions don't cause crashes
5. **Test stored procedure failures** - Simulate SP errors

File diff suppressed because it is too large Load diff

View file

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

View file

@ -19,7 +19,6 @@ import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgContro
import { DateSerializer } from "./interfaces/date-serializer"; import { DateSerializer } from "./interfaces/date-serializer";
import { initWebSocket } from "./services/webSocket"; import { initWebSocket } from "./services/webSocket";
import { RetirementService } from "./services/RetirementService";
async function main() { async function main() {
await AppDataSource.initialize(); await AppDataSource.initialize();
@ -115,17 +114,6 @@ async function main() {
} }
}); });
// Cron job for posting retirement data to Exprofile - every day at 04:30:00 on the 1st of October
const cronTime_PostRetire = "0 30 4 1 10 *";
cron.schedule(cronTime_PostRetire, async () => {
try {
const retirementService = new RetirementService();
await retirementService.cronjobPostRetireToExprofile();
} catch (error) {
console.error("[Cronjob] Error executing cronjobPostRetireToExprofile:", error);
}
});
// app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`)); // app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));
const server = app.listen( const server = app.listen(
APP_PORT, APP_PORT,

View file

@ -20,12 +20,6 @@ import { In } from "typeorm";
import { RequestWithUser } from "../middlewares/user"; import { RequestWithUser } from "../middlewares/user";
import { ApiName } from "../entities/ApiName"; import { ApiName } from "../entities/ApiName";
import { ApiHistory } from "../entities/ApiHistory"; 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"); const jwt = require("jsonwebtoken");
@Route("api/v1/org/apiKey") @Route("api/v1/org/apiKey")
@ -39,12 +33,6 @@ export class ApiKeyController extends Controller {
private apiKeyRepository = AppDataSource.getRepository(ApiKey); private apiKeyRepository = AppDataSource.getRepository(ApiKey);
private apiNameRepository = AppDataSource.getRepository(ApiName); private apiNameRepository = AppDataSource.getRepository(ApiName);
private apiHistoryRepository = AppDataSource.getRepository(ApiHistory); 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 * API JWT token
@ -163,9 +151,6 @@ export class ApiKeyController extends Controller {
relations: ["apiNames", "apiHistorys"], relations: ["apiNames", "apiHistorys"],
order: { createdAt: "DESC", apiNames: { createdAt: "DESC" } }, order: { createdAt: "DESC", apiNames: { createdAt: "DESC" } },
}); });
const orgNames = await this.buildOrgNameBatch(apiKey);
const data = apiKey.map((_data) => ({ const data = apiKey.map((_data) => ({
id: _data.id, id: _data.id,
createdAt: _data.createdAt, createdAt: _data.createdAt,
@ -178,7 +163,6 @@ export class ApiKeyController extends Controller {
dnaChild2Id: _data.dnaChild2Id, dnaChild2Id: _data.dnaChild2Id,
dnaChild3Id: _data.dnaChild3Id, dnaChild3Id: _data.dnaChild3Id,
dnaChild4Id: _data.dnaChild4Id, dnaChild4Id: _data.dnaChild4Id,
orgName: orgNames.get(_data.id),
apiNames: _data.apiNames.map((x) => ({ apiNames: _data.apiNames.map((x) => ({
id: x.id, id: x.id,
name: x.name, name: x.name,
@ -190,139 +174,10 @@ export class ApiKeyController extends Controller {
return new HttpSuccess(data); 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 Name * API Api Key
* *
* @summary Api Name (ADMIN) * @summary Api Key (ADMIN)
* *
*/ */
@Get("name") @Get("name")

View file

@ -106,10 +106,10 @@ export class ApiManageController extends Controller {
code: "organization", code: "organization",
name: "ข้อมูลโครงสร้าง", name: "ข้อมูลโครงสร้าง",
}, },
// { {
// code: "position", code: "position",
// name: "ข้อมูลอัตรากำลัง", name: "ข้อมูลอัตรากำลัง",
// }, },
]; ];
// รายการเอนทิตีทั้งหมด // รายการเอนทิตีทั้งหมด
@ -273,240 +273,59 @@ export class ApiManageController extends Controller {
description: "ข้อมูลส่วนราชการ ระดับที่ 4", description: "ข้อมูลส่วนราชการ ระดับที่ 4",
system: ["organization"], system: ["organization"],
}, },
// { {
// name: "PosMaster", name: "PosMaster",
// repository: this.posMasterRepository, repository: this.posMasterRepository,
// description: "ข้อมูลอัตรากำลัง", description: "ข้อมูลอัตรากำลัง",
// isMain: true, isMain: true,
// system: ["position"], system: ["position"],
// }, },
// { {
// name: "Position", name: "Position",
// repository: this.positionRepository, repository: this.positionRepository,
// description: "ข้อมูลตำแหน่ง", description: "ข้อมูลตำแหน่ง",
// system: ["position"], system: ["position"],
// }, },
// { {
// name: "OrgRoot", name: "OrgRoot",
// repository: this.orgRootRepository, repository: this.orgRootRepository,
// description: "ข้อมูลหน่วยงาน", description: "ข้อมูลหน่วยงาน",
// system: ["position"], system: ["position"],
// }, },
// { {
// name: "OrgChild1", name: "OrgChild1",
// repository: this.orgChild1Repository, repository: this.orgChild1Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 1", description: "ข้อมูลส่วนราชการ ระดับที่ 1",
// system: ["position"], system: ["position"],
// }, },
// { {
// name: "OrgChild2", name: "OrgChild2",
// repository: this.orgChild2Repository, repository: this.orgChild2Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 2", description: "ข้อมูลส่วนราชการ ระดับที่ 2",
// system: ["position"], system: ["position"],
// }, },
// { {
// name: "OrgChild3", name: "OrgChild3",
// repository: this.orgChild3Repository, repository: this.orgChild3Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 3", description: "ข้อมูลส่วนราชการ ระดับที่ 3",
// system: ["position"], system: ["position"],
// }, },
// { {
// name: "OrgChild4", name: "OrgChild4",
// repository: this.orgChild4Repository, repository: this.orgChild4Repository,
// description: "ข้อมูลส่วนราชการ ระดับที่ 4", description: "ข้อมูลส่วนราชการ ระดับที่ 4",
// system: ["position"], system: ["position"],
// }, },
// { {
// name: "Profile", name: "Profile",
// repository: this.profileRepository, repository: this.profileRepository,
// description: "ข้อมูลคนครอง", description: "ข้อมูลคนครอง",
// system: ["position"], system: ["position"],
// }, },
]; ];
private readonly DEFAULT_PAGE_SIZE = 10; // ขนาดหน้าเริ่มต้น private readonly DEFAULT_PAGE_SIZE = 10; // ขนาดหน้าเริ่มต้น
private readonly EXCLUDED_COLUMNS = [ private readonly EXCLUDED_COLUMNS = ["createdUserId", "lastUpdateUserId"]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์
"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 { private validateSuperAdminRole(user: any): void {
if (!user.role.includes("SUPER_ADMIN")) { if (!user.role.includes("SUPER_ADMIN")) {
@ -545,8 +364,11 @@ export class ApiManageController extends Controller {
const result = this.entities const result = this.entities
.filter((entity) => entity.system.includes(system)) .filter((entity) => entity.system.includes(system))
.map(({ name, repository, description, isMain }) => { .map(({ name, repository, description, isMain }) => ({
let columns = repository.metadata.columns tb: name,
description,
isMain: isMain || false,
propertys: repository.metadata.columns
.filter( .filter(
(column: any) => (column: any) =>
!column.isPrimary && !this.EXCLUDED_COLUMNS.includes(column.propertyName), !column.isPrimary && !this.EXCLUDED_COLUMNS.includes(column.propertyName),
@ -556,115 +378,9 @@ export class ApiManageController extends Controller {
type: typeof column.type === "string" ? column.type : "string", type: typeof column.type === "string" ? column.type : "string",
comment: column.comment, comment: column.comment,
key: column.propertyName, 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); return new HttpSuccess(result);
} catch (error) { } catch (error) {
throw new HttpError( throw new HttpError(

File diff suppressed because it is too large Load diff

View file

@ -123,25 +123,18 @@ export class AuthRoleController extends Controller {
// เช็คว่าถ้ามีค่า current_holderId ให้ลบ key สิทธิ์ใน redis // เช็คว่าถ้ามีค่า current_holderId ให้ลบ key สิทธิ์ใน redis
if (posMaster.current_holderId) { if (posMaster.current_holderId) {
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
redisClient.del("role_" + posMaster.current_holderId, (err: Error) => { redisClient.del("role_" + posMaster.current_holderId, (err: Error, response: Response) => {
if (err) console.error("Redis delete role error:", err); if (err) throw err;
}); });
redisClient.del("menu_" + posMaster.current_holderId, (err: Error) => { redisClient.del("menu_" + posMaster.current_holderId, (err: Error, response: Response) => {
if (err) console.error("Redis delete menu error:", err); if (err) throw err;
}); });
} finally {
if (redisClient) {
redisClient.quit();
}
}
} }
return new HttpSuccess(); return new HttpSuccess();
@ -267,33 +260,13 @@ export class AuthRoleController extends Controller {
return newAttr; return newAttr;
}); });
const before = structuredClone(record); 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 queryRunner = AppDataSource.createQueryRunner(); const redisClient = await this.redis.createClient({
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, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -301,11 +274,6 @@ export class AuthRoleController extends Controller {
await redisClient.flushdb(function (err: any, succeeded: any) { await redisClient.flushdb(function (err: any, succeeded: any) {
console.log(succeeded); // will be true if successfull console.log(succeeded); // will be true if successfull
}); });
} finally {
if (redisClient) {
redisClient.quit();
}
}
return new HttpSuccess(); return new HttpSuccess();
} }

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,576 +0,0 @@
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

@ -321,7 +321,6 @@ export class DevelopmentRequestController extends Controller {
} }
const orgRoot = await this.orgRootRepo.findOne({ const orgRoot = await this.orgRootRepo.findOne({
select: { select: {
id: true,
isDeputy: true isDeputy: true
}, },
where: { where: {
@ -370,8 +369,7 @@ export class DevelopmentRequestController extends Controller {
posLevelName: profile.posLevel.posLevelName, posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName, posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`, fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false, isDeputy: orgRoot?.isDeputy ?? false
orgRootId: orgRoot?.id ?? null
}) })
.catch((error) => { .catch((error) => {
console.error("Error calling API:", error); console.error("Error calling API:", error);

View file

@ -43,7 +43,6 @@ import {
CreatePosMasterHistoryOfficer, CreatePosMasterHistoryOfficer,
} from "../services/PositionService"; } from "../services/PositionService";
import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory";
import { KeycloakAttributeService } from "../services/KeycloakAttributeService";
@Route("api/v1/org/employee/pos") @Route("api/v1/org/employee/pos")
@Tags("Employee") @Tags("Employee")
@Security("bearerAuth") @Security("bearerAuth")
@ -66,7 +65,6 @@ export class EmployeePositionController extends Controller {
private child3Repository = AppDataSource.getRepository(OrgChild3); private child3Repository = AppDataSource.getRepository(OrgChild3);
private child4Repository = AppDataSource.getRepository(OrgChild4); private child4Repository = AppDataSource.getRepository(OrgChild4);
private authRoleRepo = AppDataSource.getRepository(AuthRole); private authRoleRepo = AppDataSource.getRepository(AuthRole);
private keycloakAttributeService = new KeycloakAttributeService();
/** /**
* API * API
@ -1058,11 +1056,11 @@ export class EmployeePositionController extends Controller {
let checkChildConditions: any = {}; let checkChildConditions: any = {};
let keywordAsInt: any; let keywordAsInt: any;
let searchShortName = "1=1"; let searchShortName = "1=1";
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`;
let _data = await new permission().PermissionOrgList(request, "SYS_ORG_EMP"); let _data = await new permission().PermissionOrgList(request, "SYS_ORG_EMP");
if (body.type === 0) { if (body.type === 0) {
typeCondition = { typeCondition = {
@ -1072,7 +1070,7 @@ export class EmployeePositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild1Id: IsNull(), orgChild1Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 1) { } else if (body.type === 1) {
@ -1083,7 +1081,7 @@ export class EmployeePositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild2Id: IsNull(), orgChild2Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 2) { } else if (body.type === 2) {
@ -1094,7 +1092,7 @@ export class EmployeePositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild3Id: IsNull(), orgChild3Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 3) { } else if (body.type === 3) {
@ -1105,14 +1103,14 @@ export class EmployeePositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild4Id: IsNull(), orgChild4Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 4) { } else if (body.type === 4) {
typeCondition = { typeCondition = {
orgChild4Id: body.id, orgChild4Id: body.id,
}; };
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} }
let findPosition: any; let findPosition: any;
let masterId = new Array(); let masterId = new Array();
@ -1140,8 +1138,10 @@ export class EmployeePositionController extends Controller {
select: ["posMasterId"], select: ["posMasterId"],
}); });
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId)); masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/); keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null; if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
masterId = [...new Set(masterId)]; masterId = [...new Set(masterId)];
} }
@ -1156,7 +1156,7 @@ export class EmployeePositionController extends Controller {
...(body.keyword && ...(body.keyword &&
(masterId.length > 0 (masterId.length > 0
? { id: In(masterId) } ? { id: In(masterId) }
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })), : { posMasterNo: Like(`%${body.keyword}%`) })),
}, },
]; ];
@ -1188,8 +1188,8 @@ export class EmployeePositionController extends Controller {
_data.child1 != undefined && _data.child1 != null _data.child1 != undefined && _data.child1 != null
? _data.child1[0] != null ? _data.child1[0] != null
? `posMaster.orgChild1Id IN (:...child1)` ? `posMaster.orgChild1Id IN (:...child1)`
: // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
`posMaster.orgChild1Id is null` : `posMaster.orgChild1Id is null`
: "1=1", : "1=1",
{ {
child1: _data.child1, child1: _data.child1,
@ -1224,11 +1224,10 @@ export class EmployeePositionController extends Controller {
{ {
child4: _data.child4, child4: _data.child4,
}, },
); )
if (body.keyword != null && body.keyword != "") { if (body.keyword != null && body.keyword != "") {
query query.orWhere(
.orWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.andWhere( qb.andWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
@ -1286,7 +1285,7 @@ export class EmployeePositionController extends Controller {
.andWhere(typeCondition) .andWhere(typeCondition)
.andWhere(revisionCondition); .andWhere(revisionCondition);
}), }),
); )
} }
let [posMaster, total] = await query let [posMaster, total] = await query
@ -2412,7 +2411,7 @@ export class EmployeePositionController extends Controller {
*/ */
@Post("profile/delete/{id}") @Post("profile/delete/{id}")
async deleteEmpHolder(@Path() id: string, @Request() request: RequestWithUser) { async deleteEmpHolder(@Path() id: string, @Request() request: RequestWithUser) {
await new permission().PermissionUpdate(request, "SYS_ORG_EMP"); await new permission().PermissionDelete(request, "SYS_ORG_EMP");
const dataMaster = await this.employeePosMasterRepository.findOne({ const dataMaster = await this.employeePosMasterRepository.findOne({
where: { id: id }, where: { id: id },
relations: ["positions", "orgRevision"], relations: ["positions", "orgRevision"],
@ -2436,12 +2435,6 @@ export class EmployeePositionController extends Controller {
// await this.profileRepository.save(profile); // await this.profileRepository.save(profile);
// } // }
// } // }
if (dataMaster.current_holderId) {
await this.keycloakAttributeService.clearOrgDnaAttributes(
[dataMaster.current_holderId],
"PROFILE_EMPLOYEE",
);
}
await this.employeePosMasterRepository.update(id, { await this.employeePosMasterRepository.update(id, {
isSit: false, isSit: false,
@ -2471,7 +2464,7 @@ export class EmployeePositionController extends Controller {
@Body() requestBody: { draftPositionId: string; publishPositionId: string }, @Body() requestBody: { draftPositionId: string; publishPositionId: string },
@Request() request: RequestWithUser, @Request() request: RequestWithUser,
) { ) {
await new permission().PermissionUpdate(request, "SYS_ORG_EMP"); await new permission().PermissionDelete(request, "SYS_ORG_EMP");
const findDraft = await this.orgRevisionRepository.findOne({ const findDraft = await this.orgRevisionRepository.findOne({
where: { where: {
orgRevisionIsDraft: true, orgRevisionIsDraft: true,

View file

@ -43,7 +43,6 @@ import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils"; import { setLogDataDiff } from "../interfaces/utils";
import { CreatePosMasterHistoryEmployeeTemp } from "../services/PositionService"; import { CreatePosMasterHistoryEmployeeTemp } from "../services/PositionService";
import { PosMasterEmployeeTempHistory } from "../entities/PosMasterEmployeeTempHistory"; import { PosMasterEmployeeTempHistory } from "../entities/PosMasterEmployeeTempHistory";
import { KeycloakAttributeService } from "../services/KeycloakAttributeService";
@Route("api/v1/org/employee-temp/pos") @Route("api/v1/org/employee-temp/pos")
@Tags("Employee") @Tags("Employee")
@Security("bearerAuth") @Security("bearerAuth")
@ -66,7 +65,6 @@ export class EmployeeTempPositionController extends Controller {
private child3Repository = AppDataSource.getRepository(OrgChild3); private child3Repository = AppDataSource.getRepository(OrgChild3);
private child4Repository = AppDataSource.getRepository(OrgChild4); private child4Repository = AppDataSource.getRepository(OrgChild4);
private authRoleRepo = AppDataSource.getRepository(AuthRole); private authRoleRepo = AppDataSource.getRepository(AuthRole);
private keycloakAttributeService = new KeycloakAttributeService();
/** /**
* API * API
@ -777,11 +775,11 @@ export class EmployeeTempPositionController extends Controller {
let checkChildConditions: any = {}; let checkChildConditions: any = {};
let keywordAsInt: any; let keywordAsInt: any;
let searchShortName = "1=1"; let searchShortName = "1=1";
let searchShortName0 = `CONCAT(orgRoot.orgRootShortName,' ',posMaster.posMasterNo)`; let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`;
let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName,' ',posMaster.posMasterNo)`; let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`;
let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName,' ',posMaster.posMasterNo)`; let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`;
let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName,' ',posMaster.posMasterNo)`; let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`;
let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName,' ',posMaster.posMasterNo)`; let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`;
let _data = await new permission().PermissionOrgList(request, "SYS_ORG_TEMP"); let _data = await new permission().PermissionOrgList(request, "SYS_ORG_TEMP");
if (body.type === 0) { if (body.type === 0) {
typeCondition = { typeCondition = {
@ -791,7 +789,7 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild1Id: IsNull(), orgChild1Id: IsNull(),
}; };
searchShortName = `CONCAT(orgRoot.orgRootShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 1) { } else if (body.type === 1) {
@ -802,7 +800,7 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild2Id: IsNull(), orgChild2Id: IsNull(),
}; };
searchShortName = `CONCAT(orgChild1.orgChild1ShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 2) { } else if (body.type === 2) {
@ -813,7 +811,7 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild3Id: IsNull(), orgChild3Id: IsNull(),
}; };
searchShortName = `CONCAT(orgChild2.orgChild2ShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 3) { } else if (body.type === 3) {
@ -824,14 +822,14 @@ export class EmployeeTempPositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild4Id: IsNull(), orgChild4Id: IsNull(),
}; };
searchShortName = `CONCAT(orgChild3.orgChild3ShortName,' ',posMaster.posMasterNo) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 4) { } else if (body.type === 4) {
typeCondition = { typeCondition = {
orgChild4Id: body.id, 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 findPosition: any;
let masterId = new Array(); let masterId = new Array();
@ -859,8 +857,10 @@ export class EmployeeTempPositionController extends Controller {
select: ["posMasterTempId"], select: ["posMasterTempId"],
}); });
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterTempId)); masterId = masterId.concat(findPosition.map((position: any) => position.posMasterTempId));
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/); keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null; if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
masterId = [...new Set(masterId)]; masterId = [...new Set(masterId)];
} }
@ -875,7 +875,7 @@ export class EmployeeTempPositionController extends Controller {
...(body.keyword && ...(body.keyword &&
(masterId.length > 0 (masterId.length > 0
? { id: In(masterId) } ? { id: In(masterId) }
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })), : { posMasterNo: Like(`%${body.keyword}%`) })),
}, },
]; ];
let query = AppDataSource.getRepository(EmployeeTempPosMaster) let query = AppDataSource.getRepository(EmployeeTempPosMaster)
@ -906,8 +906,8 @@ export class EmployeeTempPositionController extends Controller {
_data.child1 != undefined && _data.child1 != null _data.child1 != undefined && _data.child1 != null
? _data.child1[0] != null ? _data.child1[0] != null
? `posMaster.orgChild1Id IN (:...child1)` ? `posMaster.orgChild1Id IN (:...child1)`
: // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
`posMaster.orgChild1Id is null` : `posMaster.orgChild1Id is null`
: "1=1", : "1=1",
{ {
child1: _data.child1, child1: _data.child1,
@ -942,11 +942,10 @@ export class EmployeeTempPositionController extends Controller {
{ {
child4: _data.child4, child4: _data.child4,
}, },
); )
if (body.keyword != null && body.keyword != "") { if (body.keyword != null && body.keyword != "") {
query query.orWhere(
.orWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.andWhere( qb.andWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
@ -1004,7 +1003,7 @@ export class EmployeeTempPositionController extends Controller {
.andWhere(typeCondition) .andWhere(typeCondition)
.andWhere(revisionCondition); .andWhere(revisionCondition);
}), }),
); )
} }
let [posMaster, total] = await query let [posMaster, total] = await query
@ -2117,7 +2116,7 @@ export class EmployeeTempPositionController extends Controller {
*/ */
@Post("profile/delete/{id}") @Post("profile/delete/{id}")
async deleteEmpHolder(@Path() id: string, @Request() request: RequestWithUser) { async deleteEmpHolder(@Path() id: string, @Request() request: RequestWithUser) {
await new permission().PermissionUpdate(request, "SYS_ORG_TEMP"); await new permission().PermissionDelete(request, "SYS_ORG_TEMP");
const dataMaster = await this.employeeTempPosMasterRepository.findOne({ const dataMaster = await this.employeeTempPosMasterRepository.findOne({
where: { id: id }, where: { id: id },
relations: ["positions", "orgRevision"], relations: ["positions", "orgRevision"],
@ -2142,13 +2141,6 @@ export class EmployeeTempPositionController extends Controller {
// } // }
// } // }
if (dataMaster.current_holderId) {
await this.keycloakAttributeService.clearOrgDnaAttributes(
[dataMaster.current_holderId],
"PROFILE_EMPLOYEE",
);
}
await this.employeeTempPosMasterRepository.update(id, { await this.employeeTempPosMasterRepository.update(id, {
isSit: false, isSit: false,
next_holderId: null, next_holderId: null,
@ -2178,7 +2170,7 @@ export class EmployeeTempPositionController extends Controller {
@Body() requestBody: { draftPositionId: string; publishPositionId: string }, @Body() requestBody: { draftPositionId: string; publishPositionId: string },
@Request() request: RequestWithUser, @Request() request: RequestWithUser,
) { ) {
await new permission().PermissionUpdate(request, "SYS_ORG_TEMP"); await new permission().PermissionDelete(request, "SYS_ORG_TEMP");
const findDraft = await this.orgRevisionRepository.findOne({ const findDraft = await this.orgRevisionRepository.findOne({
where: { where: {
orgRevisionIsDraft: true, orgRevisionIsDraft: true,

View file

@ -14,8 +14,6 @@ import {
} from "tsoa"; } from "tsoa";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
import { addLogSequence } from "../interfaces/utils";
import HttpSuccess from "../interfaces/http-success";
interface CachedToken { interface CachedToken {
token: string; token: string;
@ -89,8 +87,7 @@ export class ExRetirementController extends Controller {
}, },
}); });
// return res.data; return res.data;
return new HttpSuccess(res.data.data);
} catch (error: any) { } catch (error: any) {
if (error.response?.status === 500 && retryCount < maxRetries - 1) { if (error.response?.status === 500 && retryCount < maxRetries - 1) {
TokenCache.delete(`${clientId}:${clientSecret}`); TokenCache.delete(`${clientId}:${clientSecret}`);
@ -174,7 +171,6 @@ async function getToken(ClientID: string, ClientSecret: string): Promise<string>
// function post retire data to exprofile system // function post retire data to exprofile system
export async function PostRetireToExprofile( export async function PostRetireToExprofile(
request: any,
citizenID: string, citizenID: string,
prefix: string, prefix: string,
firstName: string, firstName: string,
@ -236,21 +232,6 @@ export async function PostRetireToExprofile(
retryCount++; retryCount++;
continue; continue;
} }
// เช็ค request ก่อนเรียก addLogSequence (สำหรับ cronjob ที่ส่ง null)
if (request) {
addLogSequence(request, {
action: "request",
status: "error",
description: "unconnected to exprofile api",
request: {
method: "POST",
url: API_URL_BANGKOK + "/importData",
response: JSON.stringify(error),
},
});
}
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้"); throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
} }
} }

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ import { AppDataSource } from "../database/data-source";
import HttpSuccess from "../interfaces/http-success"; import HttpSuccess from "../interfaces/http-success";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
import { Brackets, In, IsNull, LessThanOrEqual, MoreThanOrEqual, Not } from "typeorm"; import { Brackets, In, IsNull, MoreThanOrEqual, Not } from "typeorm";
import { OrgRoot } from "../entities/OrgRoot"; import { OrgRoot } from "../entities/OrgRoot";
import { PosMaster } from "../entities/PosMaster"; import { PosMaster } from "../entities/PosMaster";
import { calculateRetireDate } from "../interfaces/utils"; import { calculateRetireDate } from "../interfaces/utils";
@ -24,19 +24,6 @@ import { viewEmployeePosMaster } from "../entities/view/viewEmployeePosMaster";
import { EmployeePosDict } from "../entities/EmployeePosDict"; import { EmployeePosDict } from "../entities/EmployeePosDict";
import { ProfileSalary } from "../entities/ProfileSalary"; import { ProfileSalary } from "../entities/ProfileSalary";
import { ProfileInsignia } from "../entities/ProfileInsignia"; import { ProfileInsignia } from "../entities/ProfileInsignia";
import { PosMasterHistory } from "../entities/PosMasterHistory";
import {
ProfileAbsentLate,
CreateProfileAbsentLate,
CreateProfileAbsentLateBatch,
} from "../entities/ProfileAbsentLate";
import { ProfileAbsentLateHistory } from "../entities/ProfileAbsentLateHistory";
import {
ProfileEmployeeAbsentLate,
CreateProfileEmployeeAbsentLate,
CreateProfileEmployeeAbsentLateBatch,
} from "../entities/ProfileEmployeeAbsentLate";
import { ProfileEmployeeAbsentLateHistory } from "../entities/ProfileEmployeeAbsentLateHistory";
@Route("api/v1/org/unauthorize") @Route("api/v1/org/unauthorize")
@Tags("OrganizationUnauthorize") @Tags("OrganizationUnauthorize")
@Response( @Response(
@ -57,12 +44,7 @@ export class OrganizationUnauthorizeController extends Controller {
private posMasterRepository = AppDataSource.getRepository(PosMaster); private posMasterRepository = AppDataSource.getRepository(PosMaster);
private empPosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); private empPosMasterRepository = AppDataSource.getRepository(EmployeePosMaster);
private salaryRepo = AppDataSource.getRepository(ProfileSalary); private salaryRepo = AppDataSource.getRepository(ProfileSalary);
private posMasterHistoryRepository = AppDataSource.getRepository(PosMasterHistory);
private insigniaRepo = AppDataSource.getRepository(ProfileInsignia); private insigniaRepo = AppDataSource.getRepository(ProfileInsignia);
private absentLateRepo = AppDataSource.getRepository(ProfileAbsentLate);
private absentLateHistoryRepo = AppDataSource.getRepository(ProfileAbsentLateHistory);
private empAbsentLateRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLate);
private empAbsentLateHistoryRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLateHistory);
@Post("user/reset-password") @Post("user/reset-password")
async forgetPassword( async forgetPassword(
@Body() @Body()
@ -3125,433 +3107,4 @@ export class OrganizationUnauthorizeController extends Controller {
await this.profileEmpRepo.save(profiles); await this.profileEmpRepo.save(profiles);
return new HttpSuccess(); return new HttpSuccess();
} }
/**
* API (unauthorize)
*
* @summary
*
*/
@Post("officer-list")
async officerList(
@Body()
body: {
reqNode?: number;
reqNodeId?: string;
date: Date;
},
) {
let typeCondition: any = {};
// Build typeCondition based on reqNode and reqNodeId (similar to OWNER/ROOT/PARENT logic)
switch (body.reqNode) {
case 0:
typeCondition = {
rootDnaId: body.reqNodeId,
};
break;
case 1:
typeCondition = {
child1DnaId: body.reqNodeId,
};
break;
case 2:
typeCondition = {
child2DnaId: body.reqNodeId,
};
break;
case 3:
typeCondition = {
child3DnaId: body.reqNodeId,
};
break;
case 4:
typeCondition = {
child4DnaId: body.reqNodeId,
};
break;
default:
typeCondition = {};
break;
}
const date = body.date ? new Date(body.date.toISOString().slice(0, 10)) : new Date();
// set เวลาเป็น 23:59:59 ของวันนั้น
date.setHours(23, 59, 59, 999);
let profile = await this.posMasterHistoryRepository.find({
where: {
...typeCondition,
createdAt: LessThanOrEqual(date),
},
order: {
firstName: "ASC",
lastName: "ASC",
createdAt: "DESC",
},
});
// group1: group by ancestorDNA แล้วเลือก create_at ล่าสุด
const grouped1 = new Map<string, PosMasterHistory>();
for (const item of profile) {
const key = `${item.ancestorDNA}`;
if (!grouped1.has(key)) {
grouped1.set(key, item);
} else {
const exist = grouped1.get(key);
if (exist && item.createdAt > exist.createdAt) {
grouped1.set(key, item);
}
}
}
// group2: group by shortName-posMasterNo จากค่าที่ได้จาก group1
const grouped2 = new Map<string, PosMasterHistory>();
for (const item of Array.from(grouped1.values())) {
const key = `${item.shortName}-${item.posMasterNo}`;
if (!grouped2.has(key)) {
grouped2.set(key, item);
} else {
const exist = grouped2.get(key);
if (exist && item.createdAt > exist.createdAt) {
grouped2.set(key, item);
}
}
}
// group3: group by firstName-lastName จากค่าที่ได้จาก group2
const grouped3 = new Map<string, PosMasterHistory>();
for (const item of Array.from(grouped2.values())) {
const key = `${item.firstName}-${item.lastName}`;
if (!grouped3.has(key)) {
grouped3.set(key, item);
} else {
const exist = grouped3.get(key);
if (exist && item.createdAt > exist.createdAt) {
grouped3.set(key, item);
}
}
}
const profile_ = await Promise.all(
Array.from(grouped3.values())
.filter((x) => x.profileId != null)
.map(async (item: PosMasterHistory) => {
let profile = await this.profileRepo.findOne({
where: { id: item.profileId },
});
return {
id: item.profileId,
prefix: item.prefix,
firstName: item.firstName,
lastName: item.lastName,
citizenId: profile?.citizenId ?? null,
dateStart: profile?.dateStart ?? null,
dateAppoint: profile?.dateAppoint ?? null,
keycloak: profile?.keycloak ?? null,
posNo: `${item.shortName} ${item.posMasterNo}`,
position: item.position,
positionLevel: item.posLevel,
positionType: item.posType,
orgRootId: item.rootDnaId,
orgChild1Id: item.child1DnaId,
orgChild2Id: item.child2DnaId,
orgChild3Id: item.child3DnaId,
orgChild4Id: item.child4DnaId,
};
}),
);
return new HttpSuccess(
(profile_ ?? []).sort((a, b) => a.posNo.localeCompare(b.posNo, undefined, { numeric: true })),
);
}
/**
* API (unauthorize)
*
* @summary
*
*/
@Post("employee-list")
async employeeList(
@Body()
body: {
reqNode?: number;
reqNodeId?: string;
startDate?: Date;
endDate?: Date;
revisionId?: string;
},
) {
let typeCondition: any = {};
// Build typeCondition based on reqNode and reqNodeId (similar to OWNER/ROOT/PARENT logic)
switch (body.reqNode) {
case 0:
typeCondition = {
orgRoot: {
id: body.reqNodeId,
},
};
break;
case 1:
typeCondition = {
orgChild1: {
id: body.reqNodeId,
},
};
break;
case 2:
typeCondition = {
orgChild2: {
id: body.reqNodeId,
},
};
break;
case 3:
typeCondition = {
orgChild3: {
id: body.reqNodeId,
},
};
break;
case 4:
typeCondition = {
orgChild4: {
id: body.reqNodeId,
},
};
break;
default:
typeCondition = {};
break;
}
let profile = await this.profileEmpRepo.find({
where: { isLeave: false, isRetirement: false, current_holders: typeCondition },
relations: [
"posType",
"posLevel",
"current_holders",
"current_holders.orgRoot",
"current_holders.orgChild1",
"current_holders.orgChild2",
"current_holders.orgChild3",
"current_holders.orgChild4",
],
order: {
current_holders: {
orgRoot: {
orgRootOrder: "ASC",
},
orgChild1: {
orgChild1Order: "ASC",
},
orgChild2: {
orgChild2Order: "ASC",
},
orgChild3: {
orgChild3Order: "ASC",
},
orgChild4: {
orgChild4Order: "ASC",
},
posMasterNo: "ASC",
},
},
});
let findRevision = await this.orgRevisionRepository.findOne({
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
if (body.revisionId) {
findRevision = await this.orgRevisionRepository.findOne({
where: { id: body.revisionId },
});
}
const profile_ = await Promise.all(
profile.map(async (item: ProfileEmployee) => {
const shortName =
item.current_holders.length == 0
? 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}`
: null;
const Oc =
item.current_holders.length == 0
? null
: item.current_holders[0].orgChild4 != null
? `${item.current_holders[0].orgChild4.orgChild4Name}/${item.current_holders[0].orgChild3.orgChild3Name}/${item.current_holders[0].orgChild2.orgChild2Name}/${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}`
: item.current_holders[0].orgChild3 != null
? `${item.current_holders[0].orgChild3.orgChild3Name}/${item.current_holders[0].orgChild2.orgChild2Name}/${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}`
: item.current_holders[0].orgChild2 != null
? `${item.current_holders[0].orgChild2.orgChild2Name}/${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}`
: item.current_holders[0].orgChild1 != null
? `${item.current_holders[0].orgChild1.orgChild1Name}/${item.current_holders[0].orgRoot.orgRootName}`
: item.current_holders[0].orgRoot != null
? `${item.current_holders[0].orgRoot.orgRootName}`
: null;
let _posMaster = await this.empPosMasterRepository.findOne({
where: {
orgRevisionId: findRevision?.id,
current_holderId: item.id,
},
});
return {
id: item.id,
prefix: item.prefix,
firstName: item.firstName,
lastName: item.lastName,
citizenId: item.citizenId,
dateStart: item.dateStart,
dateAppoint: item.dateAppoint,
keycloak: item.keycloak,
posNo: shortName,
position: item.position,
positionLevel:
item.posType?.posTypeShortName && item.posLevel?.posLevelName
? `${item.posType?.posTypeShortName} ${item.posLevel?.posLevelName}`
: null,
positionType: item.posType?.posTypeName ?? null,
oc: Oc,
orgRootId: _posMaster?.orgRootId,
orgChild1Id: _posMaster?.orgChild1Id,
orgChild2Id: _posMaster?.orgChild2Id,
orgChild3Id: _posMaster?.orgChild3Id,
orgChild4Id: _posMaster?.orgChild4Id,
};
}),
);
return new HttpSuccess(profile_);
}
/**
* API / ( schedule)
* @summary API / ( Job schedule)
*/
@Post("profile/absent-late/batch")
async newAbsentLateBatch(@Body() body: CreateProfileAbsentLateBatch) {
// กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย)
if (!body.records || body.records.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const profileIds = [...new Set(body.records.map((r) => r.profileId))];
const profiles = await this.profileRepo.findBy({
id: In(profileIds),
});
const foundProfileIds = new Set(profiles.map((p) => p.id));
const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileId));
// กรณีไม่พบ profile เลย
if (validRecords.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const meta = {
createdUserId: "SYSTEM",
createdFullName: "SYSTEM",
lastUpdateUserId: "SYSTEM",
lastUpdateFullName: "SYSTEM",
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
const records = validRecords.map((item) => {
const data = new ProfileAbsentLate();
Object.assign(data, { ...item, ...meta });
return data;
});
const result = await this.absentLateRepo.save(records);
// บันทึก history สำหรับแต่ละ record
const historyRecords = result.map((data) => {
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileAbsentLateId = data.id;
return history;
});
await this.absentLateHistoryRepo.save(historyRecords);
return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) });
}
/**
* API / ( schedule)
* @summary API / ( schedule)
*/
@Post("profile-employee/absent-late/batch")
async newEmpAbsentLateBatch(@Body() body: CreateProfileEmployeeAbsentLateBatch) {
// กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย)
if (!body.records || body.records.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const profileIds = [...new Set(body.records.map((r) => r.profileEmployeeId))];
const profiles = await this.profileEmpRepo.findBy({
id: In(profileIds),
});
const foundProfileIds = new Set(profiles.map((p) => p.id));
const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileEmployeeId));
// กรณีไม่พบ profile เลย
if (validRecords.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const meta = {
createdUserId: "SYSTEM",
createdFullName: "SYSTEM",
lastUpdateUserId: "SYSTEM",
lastUpdateFullName: "SYSTEM",
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
const records = validRecords.map((item) => {
const data = new ProfileEmployeeAbsentLate();
Object.assign(data, { ...item, ...meta });
return data;
});
const result = await this.empAbsentLateRepo.save(records);
// บันทึก history สำหรับแต่ละ record
const historyRecords = result.map((data) => {
const history = new ProfileEmployeeAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileEmployeeAbsentLateId = data.id;
return history;
});
await this.empAbsentLateHistoryRepo.save(historyRecords);
return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) });
}
} }

View file

@ -15,8 +15,6 @@ import permission from "../interfaces/permission";
import { ProfileEmployee } from "../entities/ProfileEmployee"; import { ProfileEmployee } from "../entities/ProfileEmployee";
import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { OrgRevision } from "../entities/OrgRevision"; import { OrgRevision } from "../entities/OrgRevision";
import { PosMasterAct } from "../entities/PosMasterAct";
import { actingPositionService } from "../services/ActingPositionService";
const REDIS_HOST = process.env.REDIS_HOST; const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = process.env.REDIS_PORT; const REDIS_PORT = process.env.REDIS_PORT;
@ -32,14 +30,11 @@ export class PermissionController extends Controller {
private authRoleAttrRepo = AppDataSource.getRepository(AuthRoleAttr); private authRoleAttrRepo = AppDataSource.getRepository(AuthRoleAttr);
private authSysRepo = AppDataSource.getRepository(AuthSys); private authSysRepo = AppDataSource.getRepository(AuthSys);
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
private posMasterActRepo = AppDataSource.getRepository(PosMasterAct);
private redis = require("redis"); private redis = require("redis");
@Get("") @Get("")
public async getPermission(@Request() request: RequestWithUser) { public async getPermission(@Request() request: RequestWithUser) {
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -59,7 +54,10 @@ export class PermissionController extends Controller {
} }
} }
// Query ตำแหน่งรักษาการโดยใช้ service ที่มีอยู่ let reply = await getAsync("role_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
const orgRevision = await this.orgRevisionRepository.findOne({ const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"], select: ["id"],
where: { where: {
@ -67,17 +65,6 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true, orgRevisionIsCurrent: true,
}, },
}); });
const actingData = await actingPositionService.getActingPositionsWithPrivilege(
profile.id,
orgRevision?.id
);
// ใช้ cache key เดิม และตรวจสอบสถานะ acting ทุกครั้ง
let reply = await getAsync("role_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
let posMaster: any = await this.posMasterRepository.findOne({ let posMaster: any = await this.posMasterRepository.findOne({
select: ["authRoleId"], select: ["authRoleId"],
where: { where: {
@ -93,18 +80,11 @@ export class PermissionController extends Controller {
orgRevisionId: orgRevision?.id, orgRevisionId: orgRevision?.id,
}, },
}); });
} if (!posMaster) {
// ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position)
if (!posMaster && !actingData.isAct) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
} }
}
let getDetail: any = null; const getDetail = await this.authRoleRepo.findOne({
let roleAttrData: any[] = [];
if (posMaster) {
getDetail = await this.authRoleRepo.findOne({
select: ["id", "roleName", "roleDescription"], select: ["id", "roleName", "roleDescription"],
where: { id: posMaster.authRoleId }, where: { id: posMaster.authRoleId },
}); });
@ -113,7 +93,7 @@ export class PermissionController extends Controller {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
} }
roleAttrData = await this.authRoleAttrRepo.find({ const roleAttrData = await this.authRoleAttrRepo.find({
select: [ select: [
"authSysId", "authSysId",
"parentNode", "parentNode",
@ -127,156 +107,14 @@ export class PermissionController extends Controller {
], ],
where: { authRoleId: getDetail.id }, where: { authRoleId: getDetail.id },
}); });
} else {
// ถ้าไม่มี posMaster แต่มี acting: สร้าง getDetail เปล่าๆ
getDetail = {
id: null,
roleName: "Acting",
roleDescription: "สิทธิ์จากตำแหน่งรักษาการ",
};
}
// ถ้า User มีตำแหน่งรักษาการ ให้รวมสิทธิ์
if (actingData.isAct && actingData.posMasterActs.length > 0) {
// ดึง authRoleId ของทุกตำแหน่งรักษาการ
const actingAuthRoleIds = await this.posMasterActRepo
.createQueryBuilder("posMasterAct")
.leftJoin("posMasterAct.posMaster", "posMaster")
.select("posMaster.authRoleId", "authRoleId")
.leftJoin("posMasterAct.posMasterChild", "posMasterChild")
.leftJoin("posMasterChild.current_holder", "profile")
.where("profile.id = :profileId", { profileId: profile.id })
.andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: orgRevision?.id })
.getRawMany();
// ดึง AuthRoleAttr ทั้งหมดของ acting roles
const actingRoleIds = actingAuthRoleIds.map(x => x.authRoleId).filter(id => id != null);
const actingRoleAttrs = await this.authRoleAttrRepo.find({
select: [
"authSysId",
"parentNode",
"attrOwnership",
"attrIsCreate",
"attrIsList",
"attrIsGet",
"attrIsUpdate",
"attrIsDelete",
"attrPrivilege",
],
where: { authRoleId: In(actingRoleIds) as any },
});
// สร้าง map ของ authSysId -> สิทธิ์ที่ดีที่สุดจาก acting
const actingPermissionMap = new Map<string, any>();
// ลำดับความสำคัญของ privilege (มากไปน้อย)
const privilegePriority: Record<string, number> = {
"OWNER": 7,
"PARENT": 6,
"ROOT": 5,
"BROTHER": 4,
"CHILD": 3,
"NORMAL": 2,
"SPECIFIC": 1,
"null": 0,
};
// ฟังก์ชันเปรียบเทียบ privilege
const getHigherPrivilege = (priv1: string | null, priv2: string | null): string | null => {
const p1 = priv1 ?? "null";
const p2 = priv2 ?? "null";
const priority1 = privilegePriority[p1] ?? 0;
const priority2 = privilegePriority[p2] ?? 0;
return priority1 >= priority2 ? priv1 : priv2;
};
// ฟังก์ชันเปรียบเทียบ ownership (OWNER > STAFF > null)
const getHigherOwnership = (own1: string | null, own2: string | null): string | null => {
// OWNER สูงสุด
if (own1 === "OWNER" || own2 === "OWNER") return "OWNER";
// STAFF รองลงมา
if (own1 === "STAFF" || own2 === "STAFF") return "STAFF";
return null;
};
for (const attr of actingRoleAttrs) {
const key = attr.authSysId;
if (!actingPermissionMap.has(key)) {
actingPermissionMap.set(key, attr);
} else {
// รวมสิทธิ์: ใช้ OR logic สำหรับ CRUD
// สำหรับ attrOwnership และ attrPrivilege ใช้ค่าที่ใหญ่ที่สุด
const existing = actingPermissionMap.get(key);
actingPermissionMap.set(key, {
...attr,
attrIsCreate: existing.attrIsCreate || attr.attrIsCreate,
attrIsList: existing.attrIsList || attr.attrIsList,
attrIsGet: existing.attrIsGet || attr.attrIsGet,
attrIsUpdate: existing.attrIsUpdate || attr.attrIsUpdate,
attrIsDelete: existing.attrIsDelete || attr.attrIsDelete,
attrPrivilege: getHigherPrivilege(attr.attrPrivilege, existing.attrPrivilege),
parentNode: attr.parentNode, // ใช้ parentNode ของ acting role
attrOwnership: getHigherOwnership(attr.attrOwnership, existing.attrOwnership),
});
}
}
// รวมกับสิทธิ์พื้นฐานของ User
// สำหรับระบบที่อยู่ใน acting: ใช้สิทธิ์จาก acting
// สำหรับระบบที่ไม่อยู่ใน acting: ใช้สิทธิ์พื้นฐาน
const mergedRoleAttrs = roleAttrData.map((baseAttr) => {
const actingAttr = actingPermissionMap.get(baseAttr.authSysId);
if (actingAttr) {
// ระบบนี้มีสิทธิ์จาก acting - ใช้ค่าจาก acting role
return {
...baseAttr,
parentNode: actingAttr.parentNode,
attrOwnership: getHigherOwnership(actingAttr.attrOwnership, baseAttr.attrOwnership),
attrIsCreate: actingAttr.attrIsCreate || baseAttr.attrIsCreate,
attrIsList: actingAttr.attrIsList || baseAttr.attrIsList,
attrIsGet: actingAttr.attrIsGet || baseAttr.attrIsGet,
attrIsUpdate: actingAttr.attrIsUpdate || baseAttr.attrIsUpdate,
attrIsDelete: actingAttr.attrIsDelete || baseAttr.attrIsDelete,
attrPrivilege: getHigherPrivilege(actingAttr.attrPrivilege, baseAttr.attrPrivilege),
// เพิ่ม metadata เพื่อระบุว่ามาจาก acting
_isActing: true,
};
}
// เก็บสิทธิ์พื้นฐานสำหรับระบบที่ไม่ได้รักษาการ
return baseAttr;
});
// เพิ่มระบบที่มีเฉพาะใน acting roles
for (const [authSysId, actingAttr] of actingPermissionMap) {
if (!roleAttrData.find(a => a.authSysId === authSysId)) {
mergedRoleAttrs.push({
...actingAttr,
_isActing: true,
});
}
}
reply = {
...getDetail,
roles: mergedRoleAttrs,
isActing: true, // Flag ระบุสถานะ acting
};
} else {
// ไม่มี acting - ใช้ response เดิม
reply = { reply = {
...getDetail, ...getDetail,
roles: roleAttrData, roles: roleAttrData,
isActing: false,
}; };
}
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
} }
return new HttpSuccess(reply); return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
} }
@Get("menu") @Get("menu")
@ -288,9 +126,7 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true, orgRevisionIsCurrent: true,
}, },
}); });
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -312,13 +148,6 @@ export class PermissionController extends Controller {
} }
} }
// Query ตำแหน่งรักษาการ
const actingData = await actingPositionService.getActingPositionsWithPrivilege(
profile.id,
orgRevision?.id
);
// ใช้ cache key เดิม
let reply = await getAsync("menu_" + profile.id); let reply = await getAsync("menu_" + profile.id);
if (reply != null) { if (reply != null) {
reply = JSON.parse(reply); reply = JSON.parse(reply);
@ -338,22 +167,16 @@ export class PermissionController extends Controller {
orgRevisionId: orgRevision?.id, orgRevisionId: orgRevision?.id,
}, },
}); });
} if (!posMaster) {
// ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position)
if (!posMaster && !actingData.isAct) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
} }
}
let authRole: any = null;
let roleAttrData: any[] = [];
if (posMaster) {
if (!posMaster.authRoleId) { if (!posMaster.authRoleId) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
} }
authRole = await this.authRoleRepo.findOne({ const authRole = await this.authRoleRepo.findOne({
select: ["id"], select: ["id"],
where: { id: posMaster.authRoleId }, where: { id: posMaster.authRoleId },
}); });
@ -361,48 +184,10 @@ export class PermissionController extends Controller {
if (!authRole) { if (!authRole) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
} }
const roleAttrData = await this.authRoleAttrRepo.find({
// ดึง roleAttrData ของ user ปกติ
roleAttrData = await this.authRoleAttrRepo.find({
select: ["authSysId", "parentNode"], select: ["authSysId", "parentNode"],
where: { authRoleId: authRole.id, attrIsList: true }, where: { authRoleId: authRole.id, attrIsList: true },
}); });
}
// ถ้ามี acting positions ให้รวมสิทธิ์
if (actingData.isAct && actingData.posMasterActs.length > 0) {
// ดึง authRoleId ของทุกตำแหน่งรักษาการ
const actingAuthRoleIds = await this.posMasterActRepo
.createQueryBuilder("posMasterAct")
.leftJoin("posMasterAct.posMaster", "posMaster")
.select("posMaster.authRoleId", "authRoleId")
.leftJoin("posMasterAct.posMasterChild", "posMasterChild")
.leftJoin("posMasterChild.current_holder", "profile")
.where("profile.id = :profileId", { profileId: profile.id })
.andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: orgRevision?.id })
.getRawMany();
// ดึง AuthRoleAttr ทั้งหมดของ acting roles (เฉพาะที่มี attrIsList: true)
const actingRoleIds = actingAuthRoleIds.map(x => x.authRoleId).filter(id => id != null);
const actingRoleAttrs = await this.authRoleAttrRepo.find({
select: ["authSysId", "parentNode"],
where: { authRoleId: In(actingRoleIds) as any, attrIsList: true },
});
// รวม authSysId และ parentNode จาก acting เข้ากับ base
// สำหรับระบบที่มีในทั้งสอง ให้ใช้ค่าของ acting (parentNode)
for (const actingAttr of actingRoleAttrs) {
const existingIndex = roleAttrData.findIndex(x => x.authSysId === actingAttr.authSysId);
if (existingIndex >= 0) {
// ระบบนี้มีใน base ด้วย -> ใช้ parentNode ของ acting
roleAttrData[existingIndex].parentNode = actingAttr.parentNode;
} else {
// ระบบนี้มีเฉพาะใน acting -> เพิ่มเข้าไป
roleAttrData.push(actingAttr);
}
}
}
const parentNode = roleAttrData.map((x) => x.parentNode); const parentNode = roleAttrData.map((x) => x.parentNode);
const authSysId = roleAttrData.map((x) => x.authSysId); const authSysId = roleAttrData.map((x) => x.authSysId);
const sysId = parentNode.concat(authSysId); const sysId = parentNode.concat(authSysId);
@ -447,112 +232,6 @@ export class PermissionController extends Controller {
} }
return new HttpSuccess(reply); return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
/**
* API
* @summary
* @param {string} system authSysId
*/
@Get("acting/{system}")
public async getSystemsActing(@Request() request: RequestWithUser, @Path() system: string) {
let profile: any = await this.profileRepo.findOne({
select: ["id"],
where: { keycloak: request.user.sub },
});
if (!profile) {
profile = await this.profileEmployeeRepo.findOne({
select: ["id"],
where: { keycloak: request.user.sub },
});
if (!profile) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ");
}
}
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
const posMasterActs = await this.posMasterActRepo
.createQueryBuilder("posMasterAct")
.leftJoinAndSelect("posMasterAct.posMaster", "posMaster")
.addSelect(["posMaster.authRoleId", "posMaster.posMasterNo"])
.leftJoinAndSelect("posMaster.orgRoot", "orgRoot")
.leftJoinAndSelect("posMaster.orgChild1", "orgChild1")
.leftJoinAndSelect("posMaster.orgChild2", "orgChild2")
.leftJoinAndSelect("posMaster.orgChild3", "orgChild3")
.leftJoinAndSelect("posMaster.orgChild4", "orgChild4")
.leftJoinAndSelect("posMasterAct.posMasterChild", "posMasterChild")
.leftJoinAndSelect("posMasterChild.current_holder", "profileChild")
.where("profileChild.id = :profileId", { profileId: profile.id })
.andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: orgRevision?.id })
.getMany();
if (posMasterActs.length === 0) {
return new HttpSuccess([]);
}
const results = await Promise.all(
posMasterActs.map(async (act) => {
if (!act.posMaster?.authRoleId) {
return null;
}
const roleAttrData = await this.authRoleAttrRepo.findOne({
select: [
"authSysId",
"parentNode",
"attrOwnership",
"attrIsCreate",
"attrIsList",
"attrIsGet",
"attrIsUpdate",
"attrIsDelete",
"attrPrivilege",
],
where: { authRoleId: act.posMaster.authRoleId, authSysId: system },
});
if (!roleAttrData) {
return null;
}
// const holder = act.posMaster;
// const posNo = !holder
// ? null
// : holder.orgChild4 != null
// ? `${holder.orgChild4.orgChild4ShortName} ${holder.posMasterNo}`
// : holder.orgChild3 != null
// ? `${holder.orgChild3.orgChild3ShortName} ${holder.posMasterNo}`
// : holder.orgChild2 != null
// ? `${holder.orgChild2.orgChild2ShortName} ${holder.posMasterNo}`
// : holder.orgChild1 != null
// ? `${holder.orgChild1.orgChild1ShortName} ${holder.posMasterNo}`
// : holder.orgRoot != null
// ? `${holder.orgRoot.orgRootShortName} ${holder.posMasterNo}`
// : null;
return {
...roleAttrData,
actingProfileId: act.posMaster.current_holderId,
// posNo: posNo,
};
})
);
const filteredResults = results.filter((r) => r !== null);
return new HttpSuccess(filteredResults);
} }
/** /**
@ -575,64 +254,6 @@ export class PermissionController extends Controller {
return new HttpSuccess(res); return new HttpSuccess(res);
} }
/**
* API permission with acting positions
* @summary permission with acting positions (dotnet api)
* @param {string} action action
* @param {string} system authSysId
*/
@Get("dotnet-acting/{action}/{system}")
public async dotnetActing(
@Request() req: RequestWithUser,
@Path() action: string,
@Path() system: string,
) {
if (!["CREATE", "DELETE", "GET", "LIST", "UPDATE"].includes(action)) {
throw new HttpError(HttpStatus.NOT_FOUND, "Action ไม่ถูกต้อง");
}
// ดึง privilege ตามปกติ
let privilege = await new permission().Permission(req, system.toLocaleUpperCase(), action);
// ดึงข้อมูล profile และ orgRevision
let profile: any = await this.profileRepo.findOne({
select: ["id"],
where: { keycloak: req.user.sub },
});
if (!profile) {
profile = await this.profileEmployeeRepo.findOne({
select: ["id"],
where: { keycloak: req.user.sub },
});
if (!profile) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ");
}
}
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
// ดึงข้อมูลตำแหน่งที่รักษาการ
const actingData = await actingPositionService.getActingPositionsWithPrivilege(
profile.id,
orgRevision?.id,
action,
system.toLocaleUpperCase()
);
// ส่งค่ากลับเหมือน dotnet endpoint แต่เพิ่ม isAct และ posMasterActs
return new HttpSuccess({
privilege,
isAct: actingData.isAct,
posMasterActs: actingData.posMasterActs,
});
}
/** /**
* API permission (dotnet api) * API permission (dotnet api)
* @summary permission (dotnet api) * @summary permission (dotnet api)
@ -686,9 +307,7 @@ export class PermissionController extends Controller {
@Path() system: string, @Path() system: string,
@Path() action: string, @Path() action: string,
) { ) {
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -781,11 +400,6 @@ export class PermissionController extends Controller {
} }
return new HttpSuccess(reply); return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
} }
@Get("user/{system}/{action}/{id}") @Get("user/{system}/{action}/{id}")
@ -802,9 +416,7 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true, orgRevisionIsCurrent: true,
}, },
}); });
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -889,17 +501,10 @@ export class PermissionController extends Controller {
} }
return new HttpSuccess(reply); return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
} }
public async getPermissionFunc(@Request() request: RequestWithUser) { public async getPermissionFunc(@Request() request: RequestWithUser) {
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -919,7 +524,10 @@ export class PermissionController extends Controller {
} }
} }
// Query ตำแหน่งรักษาการ let reply = await getAsync("role_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
const orgRevision = await this.orgRevisionRepository.findOne({ const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"], select: ["id"],
where: { where: {
@ -927,17 +535,6 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true, orgRevisionIsCurrent: true,
}, },
}); });
const actingData = await actingPositionService.getActingPositionsWithPrivilege(
profile.id,
orgRevision?.id
);
// ใช้ cache key เดิม
let reply = await getAsync("role_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
let posMaster: any = await this.posMasterRepository.findOne({ let posMaster: any = await this.posMasterRepository.findOne({
select: ["authRoleId"], select: ["authRoleId"],
where: { where: {
@ -953,18 +550,12 @@ export class PermissionController extends Controller {
orgRevisionId: orgRevision?.id, orgRevisionId: orgRevision?.id,
}, },
}); });
} if (!posMaster) {
// ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position)
if (!posMaster && !actingData.isAct) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์");
} }
}
let getDetail: any = null; const getDetail = await this.authRoleRepo.findOne({
let roleAttrData: any[] = [];
if (posMaster) {
getDetail = await this.authRoleRepo.findOne({
select: ["id", "roleName", "roleDescription"], select: ["id", "roleName", "roleDescription"],
where: { id: posMaster.authRoleId }, where: { id: posMaster.authRoleId },
}); });
@ -972,7 +563,7 @@ export class PermissionController extends Controller {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
} }
roleAttrData = await this.authRoleAttrRepo.find({ const roleAttrData = await this.authRoleAttrRepo.find({
select: [ select: [
"authSysId", "authSysId",
"parentNode", "parentNode",
@ -986,145 +577,14 @@ export class PermissionController extends Controller {
], ],
where: { authRoleId: getDetail.id }, where: { authRoleId: getDetail.id },
}); });
} else {
// ถ้าไม่มี posMaster แต่มี acting: สร้าง getDetail เปล่าๆ
getDetail = {
id: null,
roleName: "Acting",
roleDescription: "สิทธิ์จากตำแหน่งรักษาการ",
};
}
// ถ้ามี acting positions ให้รวมสิทธิ์
if (actingData.isAct && actingData.posMasterActs.length > 0) {
// ดึง authRoleId ของทุกตำแหน่งรักษาการ
const actingAuthRoleIds = await this.posMasterActRepo
.createQueryBuilder("posMasterAct")
.leftJoin("posMasterAct.posMaster", "posMaster")
.select("posMaster.authRoleId", "authRoleId")
.leftJoin("posMasterAct.posMasterChild", "posMasterChild")
.leftJoin("posMasterChild.current_holder", "profile")
.where("profile.id = :profileId", { profileId: profile.id })
.andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: orgRevision?.id })
.getRawMany();
// ดึง AuthRoleAttr ทั้งหมดของ acting roles
const actingRoleIds = actingAuthRoleIds.map(x => x.authRoleId).filter(id => id != null);
const actingRoleAttrs = await this.authRoleAttrRepo.find({
select: [
"authSysId",
"parentNode",
"attrOwnership",
"attrIsCreate",
"attrIsList",
"attrIsGet",
"attrIsUpdate",
"attrIsDelete",
"attrPrivilege",
],
where: { authRoleId: In(actingRoleIds) as any },
});
// ลำดับความสำคัญของ privilege (มากไปน้อย)
const privilegePriority: Record<string, number> = {
"OWNER": 7,
"PARENT": 6,
"ROOT": 5,
"BROTHER": 4,
"CHILD": 3,
"NORMAL": 2,
"SPECIFIC": 1,
"null": 0,
};
// ฟังก์ชันเปรียบเทียบ privilege
const getHigherPrivilege = (priv1: string | null, priv2: string | null): string | null => {
const p1 = priv1 ?? "null";
const p2 = priv2 ?? "null";
const priority1 = privilegePriority[p1] ?? 0;
const priority2 = privilegePriority[p2] ?? 0;
return priority1 >= priority2 ? priv1 : priv2;
};
// ฟังก์ชันเปรียบเทียบ ownership (OWNER > STAFF > null)
const getHigherOwnership = (own1: string | null, own2: string | null): string | null => {
if (own1 === "OWNER" || own2 === "OWNER") return "OWNER";
if (own1 === "STAFF" || own2 === "STAFF") return "STAFF";
return null;
};
// สร้าง map ของ authSysId -> สิทธิ์ที่ดีที่สุดจาก acting
const actingPermissionMap = new Map<string, any>();
for (const attr of actingRoleAttrs) {
const key = attr.authSysId;
if (!actingPermissionMap.has(key)) {
actingPermissionMap.set(key, attr);
} else {
const existing = actingPermissionMap.get(key);
actingPermissionMap.set(key, {
...attr,
attrIsCreate: existing.attrIsCreate || attr.attrIsCreate,
attrIsList: existing.attrIsList || attr.attrIsList,
attrIsGet: existing.attrIsGet || attr.attrIsGet,
attrIsUpdate: existing.attrIsUpdate || attr.attrIsUpdate,
attrIsDelete: existing.attrIsDelete || attr.attrIsDelete,
attrPrivilege: getHigherPrivilege(attr.attrPrivilege, existing.attrPrivilege),
parentNode: attr.parentNode,
attrOwnership: getHigherOwnership(attr.attrOwnership, existing.attrOwnership),
});
}
}
// รวมกับสิทธิ์พื้นฐานของ User
const mergedRoleAttrs = roleAttrData.map((baseAttr) => {
const actingAttr = actingPermissionMap.get(baseAttr.authSysId);
if (actingAttr) {
return {
...baseAttr,
parentNode: actingAttr.parentNode,
attrOwnership: getHigherOwnership(actingAttr.attrOwnership, baseAttr.attrOwnership),
attrIsCreate: actingAttr.attrIsCreate || baseAttr.attrIsCreate,
attrIsList: actingAttr.attrIsList || baseAttr.attrIsList,
attrIsGet: actingAttr.attrIsGet || baseAttr.attrIsGet,
attrIsUpdate: actingAttr.attrIsUpdate || baseAttr.attrIsUpdate,
attrIsDelete: actingAttr.attrIsDelete || baseAttr.attrIsDelete,
attrPrivilege: getHigherPrivilege(actingAttr.attrPrivilege, baseAttr.attrPrivilege),
_isActing: true,
};
}
return baseAttr;
});
// เพิ่มระบบที่มีเฉพาะใน acting roles
for (const [authSysId, actingAttr] of actingPermissionMap) {
if (!roleAttrData.find(a => a.authSysId === authSysId)) {
mergedRoleAttrs.push({
...actingAttr,
_isActing: true,
});
}
}
reply = {
...getDetail,
roles: mergedRoleAttrs,
};
} else {
reply = { reply = {
...getDetail, ...getDetail,
roles: roleAttrData, roles: roleAttrData,
}; };
}
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
} }
return reply; return reply;
} finally {
if (redisClient) {
redisClient.quit();
}
}
} }
public async Permission(req: RequestWithUser, system: string, action: string) { public async Permission(req: RequestWithUser, system: string, action: string) {
@ -1150,9 +610,7 @@ export class PermissionController extends Controller {
} }
public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) { public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) {
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -1174,7 +632,11 @@ export class PermissionController extends Controller {
} }
} }
// Query ตำแหน่งรักษาการ let reply = await getAsync("posMaster_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
let privilege = await this.Permission(request, system, action);
const orgRevision = await this.orgRevisionRepository.findOne({ const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"], select: ["id"],
where: { where: {
@ -1182,66 +644,15 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true, orgRevisionIsCurrent: true,
}, },
}); });
const actingData = await actingPositionService.getActingPositionsWithPrivilege(
profile.id,
orgRevision?.id
);
// ใช้ cache key เดิม
let reply = await getAsync("posMaster_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
let privilege = await this.Permission(request, system, action);
// ถ้ากำลังรักษาการ ให้ดึง org จาก acting position
if (actingData.isAct) {
// ดึงข้อมูล permission เพื่อเช็คว่าระบบนี้มาจาก acting หรือไม่
const permData: any = await this.getPermissionFunc(request);
const role = permData.roles.find((r: any) => r.authSysId === system);
if (role && role._isActing) {
// ระบบนี้มาจาก acting position ดึง org จาก acting
const actingOrgData = await this.getActingOrgScope(profile.id, orgRevision?.id, system, profileType);
reply = {
orgRootId: actingOrgData.orgRootId,
orgChild1Id: actingOrgData.orgChild1Id,
orgChild2Id: actingOrgData.orgChild2Id,
orgChild3Id: actingOrgData.orgChild3Id,
orgChild4Id: actingOrgData.orgChild4Id,
privilege: privilege,
};
} else {
// ระบบนี้มาจากตำแหน่งปกติ ใช้ org ปกติ
reply = await this.getBaseOrgScope(profile.id, orgRevision?.id, profileType, privilege);
}
} else {
// ไม่มี acting ใช้ org ปกติ
reply = await this.getBaseOrgScope(profile.id, orgRevision?.id, profileType, privilege);
}
redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply));
}
return reply;
} finally {
if (redisClient) {
redisClient.quit();
}
}
}
// Helper method: ดึง org scope จากตำแหน่งปกติ
private async getBaseOrgScope(profileId: string, orgRevisionId: string | undefined, profileType: string, privilege: any) {
if (profileType == "OFFICER") { if (profileType == "OFFICER") {
const posMaster = await this.posMasterRepository.findOne({ const posMaster = await this.posMasterRepository.findOne({
where: { where: {
current_holderId: profileId, current_holderId: profile.id,
orgRevisionId: orgRevisionId, orgRevisionId: orgRevision?.id,
}, },
}); });
if (!posMaster) { if (!posMaster) {
return { reply = {
orgRootId: null, orgRootId: null,
orgChild1Id: null, orgChild1Id: null,
orgChild2Id: null, orgChild2Id: null,
@ -1250,7 +661,7 @@ export class PermissionController extends Controller {
privilege: privilege, privilege: privilege,
}; };
} else { } else {
return { reply = {
orgRootId: posMaster.orgRootId, orgRootId: posMaster.orgRootId,
orgChild1Id: posMaster.orgChild1Id, orgChild1Id: posMaster.orgChild1Id,
orgChild2Id: posMaster.orgChild2Id, orgChild2Id: posMaster.orgChild2Id,
@ -1259,15 +670,16 @@ export class PermissionController extends Controller {
privilege: privilege, privilege: privilege,
}; };
} }
redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply));
} else { } else {
const posMaster = await this.posMasterEmpRepository.findOne({ const posMaster = await this.posMasterEmpRepository.findOne({
where: { where: {
current_holderId: profileId, current_holderId: profile.id,
orgRevisionId: orgRevisionId, orgRevisionId: orgRevision?.id,
}, },
}); });
if (!posMaster) { if (!posMaster) {
return { reply = {
orgRootId: null, orgRootId: null,
orgChild1Id: null, orgChild1Id: null,
orgChild2Id: null, orgChild2Id: null,
@ -1276,7 +688,7 @@ export class PermissionController extends Controller {
privilege: privilege, privilege: privilege,
}; };
} else { } else {
return { reply = {
orgRootId: posMaster.orgRootId, orgRootId: posMaster.orgRootId,
orgChild1Id: posMaster.orgChild1Id, orgChild1Id: posMaster.orgChild1Id,
orgChild2Id: posMaster.orgChild2Id, orgChild2Id: posMaster.orgChild2Id,
@ -1285,48 +697,10 @@ export class PermissionController extends Controller {
privilege: privilege, privilege: privilege,
}; };
} }
redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply));
} }
} }
return reply;
// Helper method: ดึง org scope จาก acting position ที่มีสิทธิ์ในระบบนั้น
private async getActingOrgScope(profileId: string, orgRevisionId: string | undefined, system: string, profileType: string) {
const repo = profileType === "OFFICER" ? this.posMasterRepository : this.posMasterEmpRepository;
const actingOrgData = await this.posMasterActRepo
.createQueryBuilder("posMasterAct")
.leftJoin("posMasterAct.posMaster", "posMaster")
.select([
"posMaster.orgRootId",
"posMaster.orgChild1Id",
"posMaster.orgChild2Id",
"posMaster.orgChild3Id",
"posMaster.orgChild4Id",
])
.leftJoin("posMasterAct.posMasterChild", "posMasterChild")
.leftJoin("posMasterChild.current_holder", "profile")
.where("profile.id = :profileId", { profileId })
.andWhere("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId })
.orderBy("posMasterAct.posMasterOrder", "ASC")
.getRawOne();
if (!actingOrgData) {
// ไม่พบ acting position คืนค่า null
return {
orgRootId: null,
orgChild1Id: null,
orgChild2Id: null,
orgChild3Id: null,
orgChild4Id: null,
};
}
return {
orgRootId: actingOrgData.orgRootId,
orgChild1Id: actingOrgData.orgChild1Id,
orgChild2Id: actingOrgData.orgChild2Id,
orgChild3Id: actingOrgData.orgChild3Id,
orgChild4Id: actingOrgData.orgChild4Id,
};
} }
public async PermissionOrg(req: RequestWithUser, system: string, action: string) { public async PermissionOrg(req: RequestWithUser, system: string, action: string) {
@ -1408,9 +782,7 @@ export class PermissionController extends Controller {
@Get("checkOrg/{keycloakId}") @Get("checkOrg/{keycloakId}")
public async checkOrg(@Path() keycloakId: string) { public async checkOrg(@Path() keycloakId: string) {
let redisClient; const redisClient = await this.redis.createClient({
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST, host: REDIS_HOST,
port: REDIS_PORT, port: REDIS_PORT,
}); });
@ -1492,10 +864,5 @@ export class PermissionController extends Controller {
// } // }
return new HttpSuccess(reply); return new HttpSuccess(reply);
} finally {
if (redisClient) {
redisClient.quit();
}
}
} }
} }

View file

@ -24,10 +24,6 @@ import Extension from "../interfaces/extension";
import { ProfileActposition } from "../entities/ProfileActposition"; import { ProfileActposition } from "../entities/ProfileActposition";
import { RequestWithUser } from "../middlewares/user"; import { RequestWithUser } from "../middlewares/user";
import { escape } from "querystring"; import { escape } from "querystring";
import { promisify } from "util";
const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = process.env.REDIS_PORT;
@Route("api/v1/org/pos/act") @Route("api/v1/org/pos/act")
@Tags("PosMasterAct") @Tags("PosMasterAct")
@ -41,7 +37,6 @@ export class PosMasterActController extends Controller {
private posMasterActRepository = AppDataSource.getRepository(PosMasterAct); private posMasterActRepository = AppDataSource.getRepository(PosMasterAct);
private posMasterRepository = AppDataSource.getRepository(PosMaster); private posMasterRepository = AppDataSource.getRepository(PosMaster);
private actpositionRepository = AppDataSource.getRepository(ProfileActposition); private actpositionRepository = AppDataSource.getRepository(ProfileActposition);
private redis = require("redis");
/** /**
* API * API
@ -97,6 +92,7 @@ export class PosMasterActController extends Controller {
return new HttpSuccess(posMasterAct); return new HttpSuccess(posMasterAct);
} }
/** /**
* API . * API .
* *
@ -129,7 +125,9 @@ export class PosMasterActController extends Controller {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้");
} }
let posId: any[] = posMasterMain.posMasterActs.map((x) => x.posMasterChildId); let posId: any[] = posMasterMain.posMasterActs.map(
(x) => x.posMasterChildId
);
posId.push(body.posmasterId); posId.push(body.posmasterId);
const query = await AppDataSource.getRepository(PosMaster) const query = await AppDataSource.getRepository(PosMaster)
@ -174,31 +172,31 @@ export class PosMasterActController extends Controller {
posMasterMain.orgRootId == null posMasterMain.orgRootId == null
? "posMaster.orgRootId IS NULL" ? "posMaster.orgRootId IS NULL"
: "posMaster.orgRootId = :orgRootId", : "posMaster.orgRootId = :orgRootId",
{ orgRootId: posMasterMain.orgRootId }, { orgRootId: posMasterMain.orgRootId }
) )
.andWhere( .andWhere(
posMasterMain.orgChild1Id == null posMasterMain.orgChild1Id == null
? "posMaster.orgChild1Id IS NULL" ? "posMaster.orgChild1Id IS NULL"
: "posMaster.orgChild1Id = :orgChild1Id", : "posMaster.orgChild1Id = :orgChild1Id",
{ orgChild1Id: posMasterMain.orgChild1Id }, { orgChild1Id: posMasterMain.orgChild1Id }
) )
.andWhere( .andWhere(
posMasterMain.orgChild2Id == null posMasterMain.orgChild2Id == null
? "posMaster.orgChild2Id IS NULL" ? "posMaster.orgChild2Id IS NULL"
: "posMaster.orgChild2Id = :orgChild2Id", : "posMaster.orgChild2Id = :orgChild2Id",
{ orgChild2Id: posMasterMain.orgChild2Id }, { orgChild2Id: posMasterMain.orgChild2Id }
) )
.andWhere( .andWhere(
posMasterMain.orgChild3Id == null posMasterMain.orgChild3Id == null
? "posMaster.orgChild3Id IS NULL" ? "posMaster.orgChild3Id IS NULL"
: "posMaster.orgChild3Id = :orgChild3Id", : "posMaster.orgChild3Id = :orgChild3Id",
{ orgChild3Id: posMasterMain.orgChild3Id }, { orgChild3Id: posMasterMain.orgChild3Id }
) )
.andWhere( .andWhere(
posMasterMain.orgChild4Id == null posMasterMain.orgChild4Id == null
? "posMaster.orgChild4Id IS NULL" ? "posMaster.orgChild4Id IS NULL"
: "posMaster.orgChild4Id = :orgChild4Id", : "posMaster.orgChild4Id = :orgChild4Id",
{ orgChild4Id: posMasterMain.orgChild4Id }, { orgChild4Id: posMasterMain.orgChild4Id }
); );
} }
} else { } else {
@ -212,7 +210,7 @@ export class PosMasterActController extends Controller {
new Brackets((qb) => { new Brackets((qb) => {
qb.where( qb.where(
`CONCAT(current_holder.prefix, current_holder.firstName, ' ', current_holder.lastName) LIKE :keyword`, `CONCAT(current_holder.prefix, current_holder.firstName, ' ', current_holder.lastName) LIKE :keyword`,
{ keyword: `%${keyword}%` }, { keyword: `%${keyword}%` }
) )
.orWhere(`current_holder.citizenId LIKE :keyword`, { .orWhere(`current_holder.citizenId LIKE :keyword`, {
keyword: `%${keyword}%`, keyword: `%${keyword}%`,
@ -230,7 +228,7 @@ export class PosMasterActController extends Controller {
' ', ' ',
posMaster.posMasterNo posMaster.posMasterNo
) LIKE :keyword`, ) LIKE :keyword`,
{ keyword: `%${keyword}%` }, { keyword: `%${keyword}%` }
) )
.orWhere(`posLevel.posLevelName LIKE :keyword`, { .orWhere(`posLevel.posLevelName LIKE :keyword`, {
keyword: `%${keyword}%`, keyword: `%${keyword}%`,
@ -240,8 +238,8 @@ export class PosMasterActController extends Controller {
}) })
.orWhere(`current_holder.position LIKE :keyword`, { .orWhere(`current_holder.position LIKE :keyword`, {
keyword: `%${keyword}%`, keyword: `%${keyword}%`,
}); })
}), })
); );
} }
@ -282,6 +280,7 @@ export class PosMasterActController extends Controller {
return new HttpSuccess({ data: data, total }); return new HttpSuccess({ data: data, total });
} }
/** /**
* API * API
* *
@ -296,7 +295,6 @@ export class PosMasterActController extends Controller {
where: { where: {
id: id, id: id,
}, },
relations: ["posMasterChild", "posMasterChild.current_holder"],
}); });
try { try {
result = await this.posMasterActRepository.delete({ id: id }); result = await this.posMasterActRepository.delete({ id: id });
@ -321,22 +319,6 @@ export class PosMasterActController extends Controller {
await this.posMasterActRepository.save(p); 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(); return new HttpSuccess();
} }
@ -786,9 +768,6 @@ export class PosMasterActController extends Controller {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรักษาการในตำแหน่งของหน่วยงานนี้"); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรักษาการในตำแหน่งของหน่วยงานนี้");
} }
// เก็บรวบรวม profileIds ทั้งหมดเพื่อ clear cache หลังจากบันทึกเสร็จ
const profileIdsToClearCache = new Set<string>();
await Promise.all( await Promise.all(
posMasterActs.map(async (posMasterAct) => { posMasterActs.map(async (posMasterAct) => {
const orgShortName = const orgShortName =
@ -803,8 +782,6 @@ export class PosMasterActController extends Controller {
const profileId = posMasterAct.posMasterChild?.current_holderId; const profileId = posMasterAct.posMasterChild?.current_holderId;
if (profileId) { if (profileId) {
profileIdsToClearCache.add(profileId);
const existingActivePositions = await this.actpositionRepository.find({ const existingActivePositions = await this.actpositionRepository.find({
select: [ select: [
"id", "id",
@ -813,7 +790,7 @@ export class PosMasterActController extends Controller {
"lastUpdateFullName", "lastUpdateFullName",
"lastUpdatedAt", "lastUpdatedAt",
"dateEnd", "dateEnd",
"isDeleted", "isDeleted"
], ],
where: { profileId, status: true, isDeleted: false }, where: { profileId, status: true, isDeleted: false },
}); });
@ -857,24 +834,6 @@ export class PosMasterActController extends Controller {
}), }),
); );
// Clear Redis cache หลังจากบันทึกข้อมูลเสร็จแล้ว
// ทำงานนอก loop เพื่อ clear รอบเดียว ไม่ใช่ทุก iteration
if (profileIdsToClearCache.size > 0) {
await Promise.all(
Array.from(profileIdsToClearCache).map(async (profileId) => {
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(); return new HttpSuccess();
} }
} }

View file

@ -38,14 +38,12 @@ import { EmployeePosLevel } from "../entities/EmployeePosLevel";
import { AuthRole } from "../entities/AuthRole"; import { AuthRole } from "../entities/AuthRole";
import { RequestWithUser } from "../middlewares/user"; import { RequestWithUser } from "../middlewares/user";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { resolveNodeLevel, setLogDataDiff, logPositionIsSelectedChange } from "../interfaces/utils"; import { resolveNodeLevel, setLogDataDiff } from "../interfaces/utils";
import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting";
import { PosMasterAssign } from "../entities/PosMasterAssign"; import { PosMasterAssign } from "../entities/PosMasterAssign";
import { Assign } from "../entities/Assign"; import { Assign } from "../entities/Assign";
import { ProfileEmployee } from "../entities/ProfileEmployee"; import { ProfileEmployee } from "../entities/ProfileEmployee";
import { PosMasterHistory } from "../entities/PosMasterHistory"; import { PosMasterHistory } from "../entities/PosMasterHistory";
import { CreatePosMasterHistoryOfficer } from "../services/PositionService"; import { CreatePosMasterHistoryOfficer } from "../services/PositionService";
import { KeycloakAttributeService } from "../services/KeycloakAttributeService";
@Route("api/v1/org/pos") @Route("api/v1/org/pos")
@Tags("Position") @Tags("Position")
@Security("bearerAuth") @Security("bearerAuth")
@ -75,7 +73,6 @@ export class PositionController extends Controller {
private authRoleRepo = AppDataSource.getRepository(AuthRole); private authRoleRepo = AppDataSource.getRepository(AuthRole);
private posMasterAssignRepo = AppDataSource.getRepository(PosMasterAssign); private posMasterAssignRepo = AppDataSource.getRepository(PosMasterAssign);
private assignRepo = AppDataSource.getRepository(Assign); private assignRepo = AppDataSource.getRepository(Assign);
private keycloakAttributeService = new KeycloakAttributeService();
/** /**
* API * API
@ -1257,15 +1254,7 @@ export class PositionController extends Controller {
) { ) {
await new permission().PermissionUpdate(request, "SYS_ORG"); await new permission().PermissionUpdate(request, "SYS_ORG");
const posMaster = await this.posMasterRepository.findOne({ const posMaster = await this.posMasterRepository.findOne({
relations: [ relations: ["positions", "orgRevision"],
"positions",
"orgRevision",
"orgRoot",
"orgChild1",
"orgChild2",
"orgChild3",
"orgChild4",
],
where: { id: id }, where: { id: id },
}); });
if (!posMaster) { if (!posMaster) {
@ -1427,17 +1416,7 @@ export class PositionController extends Controller {
requestBody.positions.map(async (x: any) => { requestBody.positions.map(async (x: any) => {
const match = posMaster.positions.find((p: any) => p.id == x.id); const match = posMaster.positions.find((p: any) => p.id == x.id);
if (match) { if (match) {
const oldValue = match.positionIsSelected; match.positionIsSelected = x.positionIsSelected ?? false;
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; match.orderNo = x.orderNo ?? null;
return match; return match;
} else { } else {
@ -1470,24 +1449,9 @@ export class PositionController extends Controller {
}), }),
); );
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit if (posMaster.orgRevision?.orgRevisionIsCurrent == true) {
if (posMaster.orgRevision?.orgRevisionIsCurrent == true && posMaster.current_holderId) {
const _profile = await this.profileRepository.findOne({
where: { id: posMaster.current_holderId },
});
if (_profile) {
_profile.posMasterNo = getPosMasterNo(posMaster);
_profile.org = getOrgFullName(posMaster);
await this.profileRepository.save(_profile);
}
}
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (posMaster.orgRevision?.orgRevisionIsCurrent == true && !posMaster.isSit) {
const _position = requestBody.positions.find((p) => p.positionIsSelected == true); const _position = requestBody.positions.find((p) => p.positionIsSelected == true);
if (_position) { if (_position) {
const _posExecutive = _position.posExecutiveId
? await this.posExecutiveRepository.findOne({ where: { id: _position.posExecutiveId } })
: null;
const current_holderId: any = posMaster.current_holderId; const current_holderId: any = posMaster.current_holderId;
const _profile = await this.profileRepository.findOne({ const _profile = await this.profileRepository.findOne({
where: { id: current_holderId }, where: { id: current_holderId },
@ -1496,10 +1460,6 @@ export class PositionController extends Controller {
_profile.position = _position.posDictName ?? _null; _profile.position = _position.posDictName ?? _null;
_profile.posTypeId = _position.posTypeId; _profile.posTypeId = _position.posTypeId;
_profile.posLevelId = _position.posLevelId; _profile.posLevelId = _position.posLevelId;
_profile.positionField = _position.posDictField ?? _null;
_profile.posExecutive = _posExecutive?.posExecutiveName ?? _null;
_profile.positionArea = _position.posDictArea ?? _null;
_profile.positionExecutiveField = _position.posDictExecutiveField ?? _null;
await this.profileRepository.save(_profile); await this.profileRepository.save(_profile);
} }
} }
@ -1688,11 +1648,11 @@ export class PositionController extends Controller {
let checkChildConditions: any = {}; let checkChildConditions: any = {};
let keywordAsInt: any; let keywordAsInt: any;
let searchShortName = "1=1"; let searchShortName = "1=1";
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`;
if (body.type != null && body.id != null) { if (body.type != null && body.id != null) {
if (body.type === 0) { if (body.type === 0) {
typeCondition = { typeCondition = {
@ -1702,7 +1662,7 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild1Id: IsNull(), orgChild1Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 1) { } else if (body.type === 1) {
@ -1713,7 +1673,7 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild2Id: IsNull(), orgChild2Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 2) { } else if (body.type === 2) {
@ -1724,7 +1684,7 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild3Id: IsNull(), orgChild3Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 3) { } else if (body.type === 3) {
@ -1735,14 +1695,14 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild4Id: IsNull(), orgChild4Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
} else { } else {
} }
} else if (body.type === 4) { } else if (body.type === 4) {
typeCondition = { typeCondition = {
orgChild4Id: body.id, orgChild4Id: body.id,
}; };
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`;
} }
} else { } else {
body.isAll = true; body.isAll = true;
@ -1787,8 +1747,10 @@ export class PositionController extends Controller {
select: ["posMasterId"], select: ["posMasterId"],
}); });
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId)); masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/); keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null; if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
masterId = [...new Set(masterId)]; masterId = [...new Set(masterId)];
//serch name สิทธิ์ //serch name สิทธิ์
@ -1821,7 +1783,7 @@ export class PositionController extends Controller {
...(body.keyword && ...(body.keyword &&
(masterId.length > 0 (masterId.length > 0
? { id: In(masterId) } ? { id: In(masterId) }
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })), : { posMasterNo: Like(`%${body.keyword}%`) })),
}, },
]; ];
let [posMaster, total] = await AppDataSource.getRepository(PosMaster) let [posMaster, total] = await AppDataSource.getRepository(PosMaster)
@ -2162,11 +2124,11 @@ export class PositionController extends Controller {
let checkChildConditions: any = {}; let checkChildConditions: any = {};
let keywordAsInt: any; let keywordAsInt: any;
let searchShortName = "1=1"; let searchShortName = "1=1";
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`;
let _data = await new permission().PermissionOrgList(request, "SYS_ORG"); let _data = await new permission().PermissionOrgList(request, "SYS_ORG");
if (body.type === 0) { if (body.type === 0) {
typeCondition = { typeCondition = {
@ -2176,7 +2138,7 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild1Id: IsNull(), orgChild1Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} }
} else if (body.type === 1) { } else if (body.type === 1) {
typeCondition = { typeCondition = {
@ -2186,7 +2148,7 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild2Id: IsNull(), orgChild2Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} }
} else if (body.type === 2) { } else if (body.type === 2) {
typeCondition = { typeCondition = {
@ -2196,7 +2158,7 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild3Id: IsNull(), orgChild3Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} }
} else if (body.type === 3) { } else if (body.type === 3) {
typeCondition = { typeCondition = {
@ -2206,13 +2168,13 @@ export class PositionController extends Controller {
checkChildConditions = { checkChildConditions = {
orgChild4Id: IsNull(), orgChild4Id: IsNull(),
}; };
searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} }
} else if (body.type === 4) { } else if (body.type === 4) {
typeCondition = { typeCondition = {
orgChild4Id: body.id, orgChild4Id: body.id,
}; };
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`;
} }
let findPosition: any; let findPosition: any;
let masterId = new Array(); let masterId = new Array();
@ -2249,8 +2211,10 @@ export class PositionController extends Controller {
select: ["posMasterId"], select: ["posMasterId"],
}); });
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId)); masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/); keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null; if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
masterId = [...new Set(masterId)]; masterId = [...new Set(masterId)];
} }
@ -2277,7 +2241,7 @@ export class PositionController extends Controller {
...(body.keyword && ...(body.keyword &&
(masterId.length > 0 (masterId.length > 0
? { id: In(masterId) } ? { id: In(masterId) }
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })), : { posMasterNo: Like(`%${body.keyword}%`) })),
}, },
]; ];
@ -2420,16 +2384,16 @@ export class PositionController extends Controller {
? "posMaster.orgRootId IN (:...root)" ? "posMaster.orgRootId IN (:...root)"
: "posMaster.orgRootId is null" : "posMaster.orgRootId is null"
: "1=1", : "1=1",
{ root: _data.root }, { root: _data.root }
) )
.andWhere( .andWhere(
_data.child1 != undefined && _data.child1 != null _data.child1 != undefined && _data.child1 != null
? _data.child1[0] != null ? _data.child1[0] != null
? "posMaster.orgChild1Id IN (:...child1)" ? "posMaster.orgChild1Id IN (:...child1)"
: // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}`
`posMaster.orgChild1Id is null` : `posMaster.orgChild1Id is null`
: "1=1", : "1=1",
{ child1: _data.child1 }, { child1: _data.child1 }
) )
.andWhere( .andWhere(
_data.child2 != undefined && _data.child2 != null _data.child2 != undefined && _data.child2 != null
@ -2460,14 +2424,13 @@ export class PositionController extends Controller {
{ {
child4: _data.child4, child4: _data.child4,
}, },
); )
// .andWhere(checkChildConditions) // .andWhere(checkChildConditions)
// .andWhere(typeCondition) // .andWhere(typeCondition)
// .andWhere(revisionCondition); // .andWhere(revisionCondition);
if (body.keyword != null && body.keyword != "") { if (body.keyword != null && body.keyword != "") {
query query.orWhere(
.orWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.andWhere( qb.andWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
@ -2766,19 +2729,7 @@ export class PositionController extends Controller {
id: data.id, id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
})); }));
// Bulk update using CASE WHEN instead of save() per row await this.posMasterRepository.save(sortData_0, { data: request });
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 }); setLogDataDiff(request, { before, after: sortData_0 });
break; break;
} }
@ -2807,19 +2758,7 @@ export class PositionController extends Controller {
id: data.id, id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
})); }));
// Bulk update using CASE WHEN instead of save() per row await this.posMasterRepository.save(sortData_1, { data: request });
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 }); setLogDataDiff(request, { before, after: sortData_1 });
break; break;
} }
@ -2848,19 +2787,7 @@ export class PositionController extends Controller {
id: data.id, id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
})); }));
// Bulk update using CASE WHEN instead of save() per row await this.posMasterRepository.save(sortData_2, { data: request });
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 }); setLogDataDiff(request, { before, after: sortData_2 });
break; break;
} }
@ -2889,19 +2816,7 @@ export class PositionController extends Controller {
id: data.id, id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
})); }));
// Bulk update using CASE WHEN instead of save() per row await this.posMasterRepository.save(sortData_3, { data: request });
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 }); setLogDataDiff(request, { before, after: sortData_3 });
break; break;
} }
@ -2930,19 +2845,7 @@ export class PositionController extends Controller {
id: data.id, id: data.id,
posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1,
})); }));
// Bulk update using CASE WHEN instead of save() per row await this.posMasterRepository.save(sortData_4, { data: request });
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 }); setLogDataDiff(request, { before, after: sortData_4 });
break; break;
} }
@ -3421,52 +3324,6 @@ export class PositionController extends Controller {
posMaster.lastUpdatedAt = new Date(); posMaster.lastUpdatedAt = new Date();
await this.posMasterRepository.save(posMaster, { data: request }); await this.posMasterRepository.save(posMaster, { data: request });
setLogDataDiff(request, { before, after: posMaster }); setLogDataDiff(request, { before, after: posMaster });
// อัพเดท org และ posMasterNo ใน profile ตลอดไม่ต้องดัก isSit
if (posMaster.current_holderId) {
const orgRevision = await this.orgRevisionRepository.findOne({
where: { id: posMaster.orgRevisionId },
});
if (orgRevision?.orgRevisionIsCurrent) {
const pmWithOrg = await this.posMasterRepository.findOne({
where: { id: posMaster.id },
relations: [
"orgRoot",
"orgChild1",
"orgChild2",
"orgChild3",
"orgChild4",
"positions",
"positions.posExecutive",
],
});
const _profile = await this.profileRepository.findOne({
where: { id: posMaster.current_holderId },
});
if (_profile && pmWithOrg) {
const _null: any = null;
_profile.posMasterNo = getPosMasterNo(pmWithOrg) ?? _null;
_profile.org = getOrgFullName(pmWithOrg) ?? _null;
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (!pmWithOrg.isSit) {
const selectedPos = (pmWithOrg as any).positions?.find(
(p: any) => p.positionIsSelected === true,
);
if (selectedPos) {
_profile.position = selectedPos.positionName ?? _null;
_profile.posTypeId = selectedPos.posTypeId ?? _null;
_profile.posLevelId = selectedPos.posLevelId ?? _null;
_profile.positionField = selectedPos.positionField ?? _null;
_profile.posExecutive =
(selectedPos as any).posExecutive?.posExecutiveName ?? _null;
_profile.positionArea = selectedPos.positionArea ?? _null;
_profile.positionExecutiveField = selectedPos.positionExecutiveField ?? _null;
}
}
await this.profileRepository.save(_profile);
}
}
}
} }
}), }),
); );
@ -3933,7 +3790,7 @@ export class PositionController extends Controller {
await new permission().PermissionUpdate(request, "SYS_ORG"); await new permission().PermissionUpdate(request, "SYS_ORG");
const dataMaster = await this.posMasterRepository.findOne({ const dataMaster = await this.posMasterRepository.findOne({
where: { id: requestBody.posMaster }, where: { id: requestBody.posMaster },
relations: ["positions", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], relations: ["positions"],
}); });
if (!dataMaster) { if (!dataMaster) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
@ -3965,22 +3822,11 @@ export class PositionController extends Controller {
if (_profile) { if (_profile) {
let _position = await this.positionRepository.findOne({ let _position = await this.positionRepository.findOne({
where: { id: requestBody.position, posMasterId: requestBody.posMaster }, where: { id: requestBody.position, posMasterId: requestBody.posMaster },
relations: ["posExecutive"],
}); });
if (_position) { if (_position) {
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
_profile.posMasterNo = getPosMasterNo(dataMaster);
_profile.org = getOrgFullName(dataMaster);
// ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (!dataMaster.isSit) {
_profile.position = _position.positionName; _profile.position = _position.positionName;
_profile.posTypeId = _position.posTypeId; _profile.posTypeId = _position.posTypeId;
_profile.posLevelId = _position.posLevelId; _profile.posLevelId = _position.posLevelId;
_profile.positionField = _position.positionField ?? _null;
_profile.posExecutive = _position.posExecutive?.posExecutiveName ?? _null;
_profile.positionArea = _position.positionArea ?? _null;
_profile.positionExecutiveField = _position.positionExecutiveField ?? _null;
}
await this.profileRepository.save(_profile); await this.profileRepository.save(_profile);
setLogDataDiff(request, { before, after: _profile }); setLogDataDiff(request, { before, after: _profile });
} }
@ -4009,7 +3855,7 @@ export class PositionController extends Controller {
*/ */
@Post("profile/delete/{id}") @Post("profile/delete/{id}")
async deleteHolder(@Path() id: string, @Request() request: RequestWithUser) { async deleteHolder(@Path() id: string, @Request() request: RequestWithUser) {
await new permission().PermissionUpdate(request, "SYS_ORG"); await new permission().PermissionDelete(request, "SYS_ORG");
const dataMaster = await this.posMasterRepository.findOne({ const dataMaster = await this.posMasterRepository.findOne({
where: { id: id }, where: { id: id },
relations: ["positions", "orgRevision"], relations: ["positions", "orgRevision"],
@ -4022,13 +3868,6 @@ export class PositionController extends Controller {
await CreatePosMasterHistoryOfficer(dataMaster.id, request, "DELETE"); await CreatePosMasterHistoryOfficer(dataMaster.id, request, "DELETE");
} }
if (dataMaster.current_holderId) {
await this.keycloakAttributeService.clearOrgDnaAttributes(
[dataMaster.current_holderId],
"PROFILE",
);
}
let _profileId: string = ""; let _profileId: string = "";
if (dataMaster?.current_holderId) { if (dataMaster?.current_holderId) {
_profileId = dataMaster?.current_holderId; _profileId = dataMaster?.current_holderId;
@ -4040,18 +3879,7 @@ export class PositionController extends Controller {
statusReport: "PENDING", 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) => { 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, { await this.positionRepository.update(position.id, {
positionIsSelected: false, positionIsSelected: false,
}); });
@ -5328,9 +5156,9 @@ export class PositionController extends Controller {
} }
/** /**
* API * API
* *
* @summary * @summary ORG_070 - (ADMIN) #56
* *
*/ */
@Post("master/position-condition") @Post("master/position-condition")
@ -5341,7 +5169,7 @@ export class PositionController extends Controller {
id: string; id: string;
revisionId: string; revisionId: string;
type: number; type: number;
isAll: boolean; // true คือเลือกเฉพาะตำแหน่งติดเงื่อนไข / false คือเลือกตำแหน่งทั้งหมด isAll: boolean;
page: number; page: number;
pageSize: number; pageSize: number;
keyword?: string; keyword?: string;
@ -5351,17 +5179,17 @@ export class PositionController extends Controller {
let checkChildConditions: any = {}; let checkChildConditions: any = {};
let keywordAsInt: any; let keywordAsInt: any;
let searchShortName = "1=1"; let searchShortName = "1=1";
let searchShortName0 = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName1 = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName2 = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName3 = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let searchShortName4 = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,''))`; let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, ""))`;
let _data = await new permission().PermissionOrgList(request, "SYS_POS_CONDITION"); let _data = await new permission().PermissionOrgList(request, "SYS_POS_CONDITION");
const orgDna = await new permission().checkDna(request, request.user.sub); const orgDna = await new permission().checkDna(request, request.user.sub);
let level: any = resolveNodeLevel(orgDna); let level: any = resolveNodeLevel(orgDna);
const cannotViewRootPosMaster = const cannotViewRootPosMaster =
_data.privilege === "PARENT" || (_data.privilege === "PARENT") ||
(_data.privilege === "BROTHER" && level > 1) || (_data.privilege === "BROTHER" && level > 1) ||
(_data.privilege === "CHILD" && level > 0) || (_data.privilege === "CHILD" && level > 0) ||
(_data.privilege === "NORMAL" && level != 0); (_data.privilege === "NORMAL" && level != 0);
@ -5393,51 +5221,51 @@ export class PositionController extends Controller {
typeCondition = { typeCondition = {
...(cannotViewRootPosMaster ? { orgRootId: null } : { orgRootId: body.id }), ...(cannotViewRootPosMaster ? { orgRootId: null } : { orgRootId: body.id }),
}; };
// if (!body.isAll) { if (!body.isAll) {
// checkChildConditions = { checkChildConditions = {
// orgChild1Id: IsNull(), orgChild1Id: IsNull(),
// }; };
// searchShortName = `CONCAT_WS(' ',orgRoot.orgRootShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else { } else {
// } }
} else if (body.type === 1) { } else if (body.type === 1) {
typeCondition = { typeCondition = {
...(cannotViewChild1PosMaster ? { orgChild1Id: null } : { orgChild1Id: body.id }), ...(cannotViewChild1PosMaster ? { orgChild1Id: null } : { orgChild1Id: body.id }),
}; };
// if (!body.isAll) { if (!body.isAll) {
// checkChildConditions = { checkChildConditions = {
// orgChild2Id: IsNull(), orgChild2Id: IsNull(),
// }; };
// searchShortName = `CONCAT_WS(' ',orgChild1.orgChild1ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else { } else {
// } }
} else if (body.type === 2) { } else if (body.type === 2) {
typeCondition = { typeCondition = {
...(cannotViewChild2PosMaster ? { orgChild2Id: null } : { orgChild2Id: body.id }), ...(cannotViewChild2PosMaster ? { orgChild2Id: null } : { orgChild2Id: body.id }),
}; };
// if (!body.isAll) { if (!body.isAll) {
// checkChildConditions = { checkChildConditions = {
// orgChild3Id: IsNull(), orgChild3Id: IsNull(),
// }; };
// searchShortName = `CONCAT_WS(' ',orgChild2.orgChild2ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else { } else {
// } }
} else if (body.type === 3) { } else if (body.type === 3) {
typeCondition = { typeCondition = {
...(cannotViewChild3PosMaster ? { orgChild3Id: null } : { orgChild3Id: body.id }), ...(cannotViewChild3PosMaster ? { orgChild3Id: null } : { orgChild3Id: body.id }),
}; };
// if (!body.isAll) { if (!body.isAll) {
// checkChildConditions = { checkChildConditions = {
// orgChild4Id: IsNull(), orgChild4Id: IsNull(),
// }; };
// searchShortName = `CONCAT_WS(' ',orgChild3.orgChild3ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
// } else { } else {
// } }
} else if (body.type === 4) { } else if (body.type === 4) {
typeCondition = { typeCondition = {
...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }), ...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }),
}; };
searchShortName = `CONCAT_WS(' ',orgChild4.orgChild4ShortName,NULLIF(posMaster.posMasterNoPrefix,''),posMaster.posMasterNo,NULLIF(posMaster.posMasterNoSuffix,'')) like '%${body.keyword}%'`; searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`;
} }
let findPosition: any; let findPosition: any;
let masterId = new Array(); let masterId = new Array();
@ -5474,8 +5302,10 @@ export class PositionController extends Controller {
select: ["posMasterId"], select: ["posMasterId"],
}); });
masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId)); masterId = masterId.concat(findPosition.map((position: any) => position.posMasterId));
const numericMatch = body.keyword == null ? null : body.keyword.match(/\d+/); keywordAsInt = body.keyword == null ? null : parseInt(body.keyword, 10);
keywordAsInt = numericMatch ? parseInt(numericMatch[0], 10) : null; if (isNaN(keywordAsInt)) {
keywordAsInt = "P@ssw0rd!z";
}
masterId = [...new Set(masterId)]; masterId = [...new Set(masterId)];
} }
@ -5502,8 +5332,8 @@ export class PositionController extends Controller {
...(body.keyword && ...(body.keyword &&
(masterId.length > 0 (masterId.length > 0
? { id: In(masterId) } ? { id: In(masterId) }
: /^\d+$/.test(body.keyword) ? { posMasterNo: keywordAsInt } : { posMasterNo: Like(`%${body.keyword}%`) })), : { posMasterNo: Like(`%${body.keyword}%`) })),
...(!body.isAll && { isCondition: true }), current_holderId: IsNull(),
}, },
]; ];
let [posMaster, total] = await AppDataSource.getRepository(PosMaster) let [posMaster, total] = await AppDataSource.getRepository(PosMaster)
@ -5572,15 +5402,15 @@ export class PositionController extends Controller {
new Brackets((qb) => { new Brackets((qb) => {
qb.andWhere( qb.andWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
? `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` ? body.isAll == false
? searchShortName
: `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'`
: "1=1", : "1=1",
) )
.andWhere(checkChildConditions) .andWhere(checkChildConditions)
.andWhere(typeCondition) .andWhere(typeCondition)
.andWhere(revisionCondition); .andWhere(revisionCondition)
if (!body.isAll) { .andWhere({ current_holderId: IsNull() });
qb.andWhere({ isCondition: true });
}
}), }),
) )
.orWhere( .orWhere(
@ -5590,10 +5420,8 @@ export class PositionController extends Controller {
) )
.andWhere(checkChildConditions) .andWhere(checkChildConditions)
.andWhere(typeCondition) .andWhere(typeCondition)
.andWhere(revisionCondition); .andWhere(revisionCondition)
if (!body.isAll) { .andWhere({ current_holderId: IsNull() });
qb.andWhere({ isCondition: true });
}
}), }),
) )
.orderBy("orgRoot.orgRootOrder", "ASC") .orderBy("orgRoot.orgRootOrder", "ASC")

View file

@ -1,277 +0,0 @@
import {
Body,
Controller,
Get,
Patch,
Path,
Post,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { AppDataSource } from "../database/data-source";
import { In } from "typeorm";
import {
ProfileAbsentLate,
CreateProfileAbsentLate,
CreateProfileAbsentLateBatch,
UpdateProfileAbsentLate,
} from "../entities/ProfileAbsentLate";
import { ProfileAbsentLateHistory } from "../entities/ProfileAbsentLateHistory";
import HttpSuccess from "../interfaces/http-success";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import { RequestWithUser } from "../middlewares/user";
import { Profile } from "../entities/Profile";
import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils";
@Route("api/v1/org/profile/absent-late")
@Tags("ProfileAbsentLate")
@Security("bearerAuth")
export class ProfileAbsentLateController extends Controller {
private profileRepo = AppDataSource.getRepository(Profile);
private absentLateRepo = AppDataSource.getRepository(ProfileAbsentLate);
private historyRepo = AppDataSource.getRepository(ProfileAbsentLateHistory);
/**
* API / user
* @summary API / user
*/
@Get("user")
public async getAbsentLateUser(@Request() request: { user: Record<string, any> }) {
const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
const record = await this.absentLateRepo.find({
where: { profileId: profile.id, isDeleted: false },
order: { stampDate: "DESC" },
});
return new HttpSuccess(record);
}
/**
* API / profileId
* @summary API / profileId
* @param profileId profile
*/
@Get("{profileId}")
public async getAbsentLate(@Path() profileId: string, @Request() req: RequestWithUser) {
let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_OFFICER");
if (_workflow == false)
await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId);
const record = await this.absentLateRepo.find({
where: { profileId, isDeleted: false },
order: { stampDate: "DESC" },
});
return new HttpSuccess(record);
}
/**
* API /
* @summary API /
*/
@Post()
public async newAbsentLate(
@Request() req: RequestWithUser,
@Body() body: CreateProfileAbsentLate,
) {
if (!body.profileId) {
throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileId");
}
const profile = await this.profileRepo.findOneBy({ id: body.profileId });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_OFFICER", profile.id);
const before = null;
const data = new ProfileAbsentLate();
const meta = {
createdUserId: req.user.sub,
createdFullName: req.user.name,
lastUpdateUserId: req.user.sub,
lastUpdateFullName: req.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
Object.assign(data, { ...body, ...meta });
// บันทึก history
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
await this.absentLateRepo.save(data, { data: req });
setLogDataDiff(req, { before, after: data });
history.profileAbsentLateId = data.id;
await this.historyRepo.save(history, { data: req });
return new HttpSuccess(data.id);
}
/**
* API / ( Job)
* @summary API / ( Job)
*/
@Post("batch")
public async newAbsentLateBatch(
@Request() req: RequestWithUser,
@Body() body: CreateProfileAbsentLateBatch,
) {
// กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย)
if (!body.records || body.records.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const profileIds = [...new Set(body.records.map((r) => r.profileId))];
const profiles = await this.profileRepo.findBy({
id: In(profileIds),
});
const foundProfileIds = new Set(profiles.map((p) => p.id));
const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileId));
// กรณีไม่พบ profile เลย
if (validRecords.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const meta = {
createdUserId: req.user.sub,
createdFullName: req.user.name,
lastUpdateUserId: req.user.sub,
lastUpdateFullName: req.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
const records = validRecords.map((item) => {
const data = new ProfileAbsentLate();
Object.assign(data, { ...item, ...meta });
return data;
});
const result = await this.absentLateRepo.save(records, { data: req });
// บันทึก history สำหรับแต่ละ record
const historyRecords = result.map((data) => {
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileAbsentLateId = data.id;
return history;
});
await this.historyRepo.save(historyRecords, { data: req });
return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) });
}
/**
* API /
* @summary API /
* @param absentLateId /
*/
@Patch("{absentLateId}")
public async editAbsentLate(
@Request() req: RequestWithUser,
@Body() body: UpdateProfileAbsentLate,
@Path() absentLateId: string,
) {
const record = await this.absentLateRepo.findOneBy({ id: absentLateId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
await new permission().PermissionOrgUserUpdate(
req,
"SYS_REGISTRY_OFFICER",
record.profileId,
);
const before = structuredClone(record);
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...record, id: undefined });
Object.assign(record, body);
Object.assign(history, { ...record, id: undefined });
history.profileAbsentLateId = absentLateId;
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
record.lastUpdatedAt = new Date();
history.lastUpdateUserId = req.user.sub;
history.lastUpdateFullName = req.user.name;
history.createdUserId = req.user.sub;
history.createdFullName = req.user.name;
history.createdAt = new Date();
history.lastUpdatedAt = new Date();
await Promise.all([
this.absentLateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.historyRepo.save(history, { data: req }),
]);
return new HttpSuccess();
}
/**
* API / (Soft Delete)
* @summary API / (Soft Delete)
* @param absentLateId /
*/
@Patch("update-delete/{absentLateId}")
public async updateIsDeleted(
@Request() req: RequestWithUser,
@Path() absentLateId: string,
) {
const record = await this.absentLateRepo.findOneBy({ id: absentLateId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
if (record.isDeleted === true) {
return new HttpSuccess();
}
await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_OFFICER", record.profileId);
const before = structuredClone(record);
const history = new ProfileAbsentLateHistory();
Object.assign(history, { ...record, id: undefined });
record.isDeleted = true;
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
record.lastUpdatedAt = new Date();
history.profileAbsentLateId = absentLateId;
history.isDeleted = true;
history.lastUpdateUserId = req.user.sub;
history.lastUpdateFullName = req.user.name;
history.lastUpdatedAt = new Date();
await Promise.all([
this.absentLateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.historyRepo.save(history, { data: req }),
]);
return new HttpSuccess();
}
/**
* API /
* @summary API /
* @param absentLateId /
*/
@Get("history/{absentLateId}")
public async getHistory(@Path() absentLateId: string, @Request() req: RequestWithUser) {
const record = await this.absentLateRepo.findOneBy({ id: absentLateId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", record.profileId);
const history = await this.historyRepo.find({
where: { profileAbsentLateId: absentLateId },
order: { createdAt: "DESC" },
});
return new HttpSuccess(history);
}
}

View file

@ -25,7 +25,6 @@ import {
} from "../entities/ProfileChangeName"; } from "../entities/ProfileChangeName";
import { updateName } from "../keycloak"; import { updateName } from "../keycloak";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { updateHolderProfileHistory } from "../services/PositionService";
import { setLogDataDiff } from "../interfaces/utils"; import { setLogDataDiff } from "../interfaces/utils";
@Route("api/v1/org/profile/changeName") @Route("api/v1/org/profile/changeName")
@Tags("ProfileChangeName") @Tags("ProfileChangeName")
@ -116,7 +115,7 @@ export class ProfileChangeNameController extends Controller {
await this.profileRepository.save(profile, { data: req }); await this.profileRepository.save(profile, { data: req });
setLogDataDiff(req, { before, after: profile }); setLogDataDiff(req, { before, after: profile });
if (profile != null && profile.keycloak != null && profile.isDelete === false) { if (profile != null && profile.keycloak != null) {
const result = await updateName( const result = await updateName(
profile.keycloak, profile.keycloak,
profile.firstName, profile.firstName,
@ -128,9 +127,6 @@ export class ProfileChangeNameController extends Controller {
} }
} }
// บันทึกประวัติคนครองตำแหน่ง (ถ้า profile นี้ครองตำแหน่งอยู่)
await updateHolderProfileHistory(profile.id, req);
return new HttpSuccess(data.id); return new HttpSuccess(data.id);
} }
@ -190,7 +186,7 @@ export class ProfileChangeNameController extends Controller {
} }
// ปิดไว้ก่อนเพราะ error ต้องใช้ keycloak ที่มีสิทธิ์ในการ update //update 17/07 // ปิดไว้ก่อนเพราะ error ต้องใช้ keycloak ที่มีสิทธิ์ในการ update //update 17/07
if (profile != null && profile.keycloak != null && profile.isDelete === false) { if (profile != null && profile.keycloak != null) {
const result = await updateName( const result = await updateName(
profile.keycloak, profile.keycloak,
profile.firstName, profile.firstName,

View file

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

View file

@ -113,7 +113,7 @@ export class ProfileChangeNameEmployeeTempController extends Controller {
await this.profileEmployeeRepo.save(profile, { data: req }); await this.profileEmployeeRepo.save(profile, { data: req });
setLogDataDiff(req, { before, after: profile }); setLogDataDiff(req, { before, after: profile });
if (profile != null && profile.keycloak != null && profile.isDelete === false) { if (profile != null && profile.keycloak != null) {
const result = await updateName( const result = await updateName(
profile.keycloak, profile.keycloak,
profile.firstName, profile.firstName,

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,6 @@ import {
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { DevelopmentProject } from "../entities/DevelopmentProject"; import { DevelopmentProject } from "../entities/DevelopmentProject";
import { In, Brackets } from "typeorm"; import { In, Brackets } from "typeorm";
import { setLogDataDiff } from "../interfaces/utils";
@Route("api/v1/org/profile-employee/development") @Route("api/v1/org/profile-employee/development")
@Tags("ProfileDevelopment") @Tags("ProfileDevelopment")
@Security("bearerAuth") @Security("bearerAuth")
@ -274,44 +273,6 @@ export class ProfileDevelopmentEmployeeController extends Controller {
return new HttpSuccess(); return new HttpSuccess();
} }
/**
* API IDP
* @summary API IDP
* @param developmentId IDP
*/
@Patch("update-delete/{developmentId}")
public async updateIsDeletedTraining(
@Request() req: RequestWithUser,
@Path() developmentId: string,
) {
const record = await this.developmentRepository.findOneBy({ id: developmentId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
if (record.isDeleted === true) {
return new HttpSuccess();
}
await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId);
const before = structuredClone(record);
const history = new ProfileDevelopmentHistory();
const now = new Date();
record.isDeleted = true;
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
record.lastUpdatedAt = now;
Object.assign(history, { ...record, id: undefined });
history.createdUserId = req.user.sub;
history.createdFullName = req.user.name;
history.createdAt = now;
await Promise.all([
this.developmentRepository.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.developmentHistoryRepository.save(history, { data: req }),
]);
return new HttpSuccess();
}
@Delete("{developmentId}") @Delete("{developmentId}")
public async deleteDevelopment(@Path() developmentId: string, @Request() req: RequestWithUser) { public async deleteDevelopment(@Path() developmentId: string, @Request() req: RequestWithUser) {
const _record = await this.developmentRepository.findOneBy({ id: developmentId }); const _record = await this.developmentRepository.findOneBy({ id: developmentId });

View file

@ -24,7 +24,6 @@ import CallAPI from "../interfaces/call-api";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { OrgRevision } from "../entities/OrgRevision"; import { OrgRevision } from "../entities/OrgRevision";
import { OrgRoot } from "../entities/OrgRoot"; import { OrgRoot } from "../entities/OrgRoot";
import { PosMaster } from "../entities/PosMaster";
@Route("api/v1/org/profile/edit") @Route("api/v1/org/profile/edit")
@Tags("ProfileEdit") @Tags("ProfileEdit")
@Security("bearerAuth") @Security("bearerAuth")
@ -33,7 +32,6 @@ export class ProfileEditController extends Controller {
private profileEditRepo = AppDataSource.getRepository(ProfileEdit); private profileEditRepo = AppDataSource.getRepository(ProfileEdit);
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
private orgRootRepo = AppDataSource.getRepository(OrgRoot); private orgRootRepo = AppDataSource.getRepository(OrgRoot);
private posMasterRepo = AppDataSource.getRepository(PosMaster);
@Get("user") @Get("user")
public async detailProfileEditUser( public async detailProfileEditUser(
@ -274,22 +272,6 @@ export class ProfileEditController extends Controller {
if (!getProfileEdit) { if (!getProfileEdit) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
} }
let orgRoot: OrgRoot | null = null;
if(getProfileEdit.profile) {
const empPosMaster = await this.posMasterRepo.findOne({
where: {
current_holderId: getProfileEdit.profile.id,
orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }
},
relations: { orgRevision: true }
});
if(empPosMaster) {
orgRoot = await this.orgRootRepo.findOne({
select: { isDeputy: true },
where: { id: empPosMaster.orgRootId ?? "" }
});
}
}
const _data = { const _data = {
id: getProfileEdit.id, id: getProfileEdit.id,
topic: getProfileEdit.topic, topic: getProfileEdit.topic,
@ -307,7 +289,6 @@ export class ProfileEditController extends Controller {
(getProfileEdit?.profile?.firstName ?? "") + (getProfileEdit?.profile?.firstName ?? "") +
" " + " " +
(getProfileEdit?.profile?.lastName ?? ""), (getProfileEdit?.profile?.lastName ?? ""),
isDeputy: orgRoot?.isDeputy ?? false
}; };
return new HttpSuccess(_data); return new HttpSuccess(_data);
} }
@ -335,7 +316,6 @@ export class ProfileEditController extends Controller {
} }
const orgRoot = await this.orgRootRepo.findOne({ const orgRoot = await this.orgRootRepo.findOne({
select: { select: {
id: true,
isDeputy: true isDeputy: true
}, },
where: { where: {
@ -364,8 +344,7 @@ export class ProfileEditController extends Controller {
posLevelName: profile.posLevel.posLevelName, posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName, posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`, fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false, isDeputy: orgRoot?.isDeputy ?? false
orgRootId: orgRoot?.id ?? null
}) })
.catch((error) => { .catch((error) => {
console.error("Error calling API:", error); console.error("Error calling API:", error);

View file

@ -28,7 +28,6 @@ import permission from "../interfaces/permission";
import { OrgRevision } from "../entities/OrgRevision"; import { OrgRevision } from "../entities/OrgRevision";
import { OrgRoot } from "../entities/OrgRoot"; import { OrgRoot } from "../entities/OrgRoot";
import CallAPI from "../interfaces/call-api"; import CallAPI from "../interfaces/call-api";
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
@Route("api/v1/org/profile-employee/edit") @Route("api/v1/org/profile-employee/edit")
@Tags("ProfileEmployeeEdit") @Tags("ProfileEmployeeEdit")
@Security("bearerAuth") @Security("bearerAuth")
@ -37,7 +36,6 @@ export class ProfileEditEmployeeController extends Controller {
private profileEditRepository = AppDataSource.getRepository(ProfileEdit); private profileEditRepository = AppDataSource.getRepository(ProfileEdit);
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
private orgRootRepo = AppDataSource.getRepository(OrgRoot); private orgRootRepo = AppDataSource.getRepository(OrgRoot);
private empPosMasterRepo = AppDataSource.getRepository(EmployeePosMaster);
@Get("user") @Get("user")
public async detailProfileEditUserEmp( public async detailProfileEditUserEmp(
@ -273,22 +271,6 @@ export class ProfileEditEmployeeController extends Controller {
if (!getProfileEdit) { if (!getProfileEdit) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
} }
let orgRoot: OrgRoot | null = null;
if(getProfileEdit.profileEmployee) {
const empPosMaster = await this.empPosMasterRepo.findOne({
where: {
current_holderId: getProfileEdit.profileEmployee.id,
orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }
},
relations: { orgRevision: true }
});
if(empPosMaster) {
orgRoot = await this.orgRootRepo.findOne({
select: { isDeputy: true },
where: { id: empPosMaster.orgRootId ?? "" }
});
}
}
const _data = { const _data = {
id: getProfileEdit.id, id: getProfileEdit.id,
topic: getProfileEdit.topic, topic: getProfileEdit.topic,
@ -306,7 +288,6 @@ export class ProfileEditEmployeeController extends Controller {
(getProfileEdit?.profileEmployee?.firstName ?? "") + (getProfileEdit?.profileEmployee?.firstName ?? "") +
" " + " " +
(getProfileEdit?.profileEmployee?.lastName ?? ""), (getProfileEdit?.profileEmployee?.lastName ?? ""),
isDeputy: orgRoot?.isDeputy ?? false
}; };
return new HttpSuccess(_data); return new HttpSuccess(_data);
} }
@ -336,7 +317,6 @@ export class ProfileEditEmployeeController extends Controller {
} }
const orgRoot = await this.orgRootRepo.findOne({ const orgRoot = await this.orgRootRepo.findOne({
select: { select: {
id: true,
isDeputy: true isDeputy: true
}, },
where: { where: {
@ -364,8 +344,7 @@ export class ProfileEditEmployeeController extends Controller {
posLevelName: "EMP", posLevelName: "EMP",
posTypeName: "EMP", posTypeName: "EMP",
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`, fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false, isDeputy: orgRoot?.isDeputy ?? false
orgRootId: orgRoot?.id ?? null
}) })
.catch((error) => { .catch((error) => {
console.error("Error calling API:", error); console.error("Error calling API:", error);

View file

@ -1,277 +0,0 @@
import {
Body,
Controller,
Get,
Patch,
Path,
Post,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { AppDataSource } from "../database/data-source";
import { In } from "typeorm";
import {
ProfileEmployeeAbsentLate,
CreateProfileEmployeeAbsentLate,
CreateProfileEmployeeAbsentLateBatch,
UpdateProfileEmployeeAbsentLate,
} from "../entities/ProfileEmployeeAbsentLate";
import { ProfileEmployeeAbsentLateHistory } from "../entities/ProfileEmployeeAbsentLateHistory";
import HttpSuccess from "../interfaces/http-success";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import { RequestWithUser } from "../middlewares/user";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils";
@Route("api/v1/org/profile-employee/absent-late")
@Tags("ProfileEmployeeAbsentLate")
@Security("bearerAuth")
export class ProfileEmployeeAbsentLateController extends Controller {
private profileRepo = AppDataSource.getRepository(ProfileEmployee);
private absentLateRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLate);
private historyRepo = AppDataSource.getRepository(ProfileEmployeeAbsentLateHistory);
/**
* API / user
* @summary API / user
*/
@Get("user")
public async getAbsentLateUser(@Request() request: { user: Record<string, any> }) {
const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
const record = await this.absentLateRepo.find({
where: { profileEmployeeId: profile.id, isDeleted: false },
order: { stampDate: "DESC" },
});
return new HttpSuccess(record);
}
/**
* API / profileId
* @summary API / profileId
* @param profileId profile
*/
@Get("{profileId}")
public async getAbsentLate(@Path() profileId: string, @Request() req: RequestWithUser) {
let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_EMP");
if (_workflow == false)
await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", profileId);
const record = await this.absentLateRepo.find({
where: { profileEmployeeId: profileId, isDeleted: false },
order: { stampDate: "DESC" },
});
return new HttpSuccess(record);
}
/**
* API /
* @summary API /
*/
@Post()
public async newAbsentLate(
@Request() req: RequestWithUser,
@Body() body: CreateProfileEmployeeAbsentLate,
) {
if (!body.profileEmployeeId) {
throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileEmployeeId");
}
const profile = await this.profileRepo.findOneBy({ id: body.profileEmployeeId });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_EMP", profile.id);
const before = null;
const data = new ProfileEmployeeAbsentLate();
const meta = {
createdUserId: req.user.sub,
createdFullName: req.user.name,
lastUpdateUserId: req.user.sub,
lastUpdateFullName: req.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
Object.assign(data, { ...body, ...meta });
// บันทึก history
const history = new ProfileEmployeeAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
await this.absentLateRepo.save(data, { data: req });
setLogDataDiff(req, { before, after: data });
history.profileEmployeeAbsentLateId = data.id;
await this.historyRepo.save(history, { data: req });
return new HttpSuccess(data.id);
}
/**
* API / ( Job)
* @summary API / ( Job)
*/
@Post("batch")
public async newAbsentLateBatch(
@Request() req: RequestWithUser,
@Body() body: CreateProfileEmployeeAbsentLateBatch,
) {
// กรณีไม่มีข้อมูลส่งมา (วันที่ไม่มีคนขาด/มาสาย)
if (!body.records || body.records.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const profileIds = [...new Set(body.records.map((r) => r.profileEmployeeId))];
const profiles = await this.profileRepo.findBy({
id: In(profileIds),
});
const foundProfileIds = new Set(profiles.map((p) => p.id));
const validRecords = body.records.filter((r) => foundProfileIds.has(r.profileEmployeeId));
// กรณีไม่พบ profile เลย
if (validRecords.length === 0) {
return new HttpSuccess({ count: 0, ids: [] });
}
const meta = {
createdUserId: req.user.sub,
createdFullName: req.user.name,
lastUpdateUserId: req.user.sub,
lastUpdateFullName: req.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
const records = validRecords.map((item) => {
const data = new ProfileEmployeeAbsentLate();
Object.assign(data, { ...item, ...meta });
return data;
});
const result = await this.absentLateRepo.save(records, { data: req });
// บันทึก history สำหรับแต่ละ record
const historyRecords = result.map((data) => {
const history = new ProfileEmployeeAbsentLateHistory();
Object.assign(history, { ...data, id: undefined });
history.profileEmployeeAbsentLateId = data.id;
return history;
});
await this.historyRepo.save(historyRecords, { data: req });
return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) });
}
/**
* API /
* @summary API /
* @param absentLateId /
*/
@Patch("{absentLateId}")
public async editAbsentLate(
@Request() req: RequestWithUser,
@Body() body: UpdateProfileEmployeeAbsentLate,
@Path() absentLateId: string,
) {
const record = await this.absentLateRepo.findOneBy({ id: absentLateId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
await new permission().PermissionOrgUserUpdate(
req,
"SYS_REGISTRY_EMP",
record.profileEmployeeId,
);
const before = structuredClone(record);
const history = new ProfileEmployeeAbsentLateHistory();
Object.assign(history, { ...record, id: undefined });
Object.assign(record, body);
Object.assign(history, { ...record, id: undefined });
history.profileEmployeeAbsentLateId = absentLateId;
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
record.lastUpdatedAt = new Date();
history.lastUpdateUserId = req.user.sub;
history.lastUpdateFullName = req.user.name;
history.createdUserId = req.user.sub;
history.createdFullName = req.user.name;
history.createdAt = new Date();
history.lastUpdatedAt = new Date();
await Promise.all([
this.absentLateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.historyRepo.save(history, { data: req }),
]);
return new HttpSuccess();
}
/**
* API / (Soft Delete)
* @summary API / (Soft Delete)
* @param absentLateId /
*/
@Patch("update-delete/{absentLateId}")
public async updateIsDeleted(
@Request() req: RequestWithUser,
@Path() absentLateId: string,
) {
const record = await this.absentLateRepo.findOneBy({ id: absentLateId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
if (record.isDeleted === true) {
return new HttpSuccess();
}
await new permission().PermissionOrgUserDelete(req, "SYS_REGISTRY_EMP", record.profileEmployeeId);
const before = structuredClone(record);
const history = new ProfileEmployeeAbsentLateHistory();
Object.assign(history, { ...record, id: undefined });
record.isDeleted = true;
record.lastUpdateUserId = req.user.sub;
record.lastUpdateFullName = req.user.name;
record.lastUpdatedAt = new Date();
history.profileEmployeeAbsentLateId = absentLateId;
history.isDeleted = true;
history.lastUpdateUserId = req.user.sub;
history.lastUpdateFullName = req.user.name;
history.lastUpdatedAt = new Date();
await Promise.all([
this.absentLateRepo.save(record, { data: req }),
setLogDataDiff(req, { before, after: record }),
this.historyRepo.save(history, { data: req }),
]);
return new HttpSuccess();
}
/**
* API /
* @summary API /
* @param absentLateId /
*/
@Get("history/{absentLateId}")
public async getHistory(@Path() absentLateId: string, @Request() req: RequestWithUser) {
const record = await this.absentLateRepo.findOneBy({ id: absentLateId });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_EMP", record.profileEmployeeId);
const history = await this.historyRepo.find({
where: { profileEmployeeAbsentLateId: absentLateId },
order: { createdAt: "DESC" },
});
return new HttpSuccess(history);
}
}

File diff suppressed because it is too large Load diff

View file

@ -70,6 +70,7 @@ import { deleteUser } from "../keycloak";
import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory";
import { getTopDegrees } from "../services/PositionService"; import { getTopDegrees } from "../services/PositionService";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
import { PostRetireToExprofile } from "./ExRetirementController";
@Route("api/v1/org/profile-temp") @Route("api/v1/org/profile-temp")
@Tags("ProfileEmployee") @Tags("ProfileEmployee")
@Security("bearerAuth") @Security("bearerAuth")
@ -1001,24 +1002,6 @@ export class ProfileEmployeeTempController extends Controller {
} }
const record = await this.profileRepo.findOneBy({ id }); 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 (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์นี้");
if (body.employeeClass == null || body.employeeClass == undefined || body.employeeClass == "") { if (body.employeeClass == null || body.employeeClass == undefined || body.employeeClass == "") {
@ -1312,13 +1295,13 @@ export class ProfileEmployeeTempController extends Controller {
@Get("history/user") @Get("history/user")
async getHistoryProfileByUser(@Request() request: RequestWithUser) { async getHistoryProfileByUser(@Request() request: RequestWithUser) {
const profile = await this.profileRepo.findOne({ const profile = await this.profileRepo.findOne({
where: { keycloak: request.user.sub }, where: { keycloak: request.user.sub }
}); });
if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const profileHistory = await this.profileHistoryRepo.find({ const profileHistory = await this.profileHistoryRepo.find({
where: { profileEmployeeId: profile.id }, where: { profileEmployeeId: profile.id },
order: { createdAt: "ASC" }, order: { createdAt: "ASC" }
}); });
if (profileHistory.length == 0) { if (profileHistory.length == 0) {
@ -1327,12 +1310,12 @@ export class ProfileEmployeeTempController extends Controller {
...profile, ...profile,
birthDateOld: profile?.birthDate, birthDateOld: profile?.birthDate,
profileEmployeeId: profile.id, profileEmployeeId: profile.id,
id: undefined, id: undefined
}), }),
); );
const firstRecord = await this.profileHistoryRepo.find({ const firstRecord = await this.profileHistoryRepo.find({
where: { profileEmployeeId: profile.id }, where: { profileEmployeeId: profile.id },
order: { createdAt: "ASC" }, order: { createdAt: "ASC" }
}); });
return new HttpSuccess(firstRecord); return new HttpSuccess(firstRecord);
} }
@ -1845,13 +1828,13 @@ export class ProfileEmployeeTempController extends Controller {
async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) { async getProfileHistory(@Path() id: string, @Request() req: RequestWithUser) {
// await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP");//ไม่แน่ใจTEMPปิดไว้ก่อน // await new permission().PermissionGet(req, "SYS_REGISTRY_TEMP");//ไม่แน่ใจTEMPปิดไว้ก่อน
const profile = await this.profileRepo.findOne({ const profile = await this.profileRepo.findOne({
where: { id: id }, where: { id: id }
}); });
if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const profileHistory = await this.profileHistoryRepo.find({ const profileHistory = await this.profileHistoryRepo.find({
where: { profileEmployeeId: id }, where: { profileEmployeeId: id },
order: { createdAt: "ASC" }, order: { createdAt: "ASC" }
}); });
if (profileHistory.length == 0) { if (profileHistory.length == 0) {
@ -1860,12 +1843,12 @@ export class ProfileEmployeeTempController extends Controller {
...profile, ...profile,
birthDateOld: profile?.birthDate, birthDateOld: profile?.birthDate,
profileEmployeeId: id, profileEmployeeId: id,
id: undefined, id: undefined
}), }),
); );
const firstRecord = await this.profileHistoryRepo.find({ const firstRecord = await this.profileHistoryRepo.find({
where: { profileEmployeeId: id }, where: { profileEmployeeId: id },
order: { createdAt: "ASC" }, order: { createdAt: "ASC" }
}); });
return new HttpSuccess(firstRecord); return new HttpSuccess(firstRecord);
} }
@ -3476,9 +3459,9 @@ export class ProfileEmployeeTempController extends Controller {
} }
/** /**
* API * API
* *
* @summary (ADMIN) * @summary (ADMIN)
* *
* @param {string} id Id * @param {string} id Id
*/ */
@ -3600,14 +3583,12 @@ export class ProfileEmployeeTempController extends Controller {
// profile.position = _null; // profile.position = _null;
// profile.posLevelId = _null; // profile.posLevelId = _null;
// profile.posTypeId = _null; // profile.posTypeId = _null;
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete == false) { if (profile.keycloak != null) {
const delUserKeycloak = await deleteUser(profile.keycloak); const delUserKeycloak = await deleteUser(profile.keycloak);
if (delUserKeycloak) { if (delUserKeycloak) {
// Task #228 profile.keycloak = _null;
// profile.keycloak = _null;
profile.roleKeycloaks = []; profile.roleKeycloaks = [];
profile.isActive = false; profile.isActive = false;
profile.isDelete = true;
} }
} }
await this.profileRepo.save(profile); await this.profileRepo.save(profile);
@ -3625,6 +3606,19 @@ export class ProfileEmployeeTempController extends Controller {
].filter(Boolean); ].filter(Boolean);
organizeName = names.join(" "); organizeName = names.join(" ");
} }
await PostRetireToExprofile(
profile.citizenId ?? "",
profile.prefix ?? "",
profile.firstName ?? "",
profile.lastName ?? "",
requestBody.dateLeave?.getFullYear().toString() ?? "",
profile.position,
profile.posType?.posTypeName ?? "",
`${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`,
requestBody.dateLeave ?? new Date(),
organizeName,
"ถึงแก่กรรม",
);
return new HttpSuccess(); return new HttpSuccess();
} }
@ -3992,7 +3986,7 @@ export class ProfileEmployeeTempController extends Controller {
case "citizenId": case "citizenId":
[findProfile, total] = await this.profileRepo.findAndCount({ [findProfile, total] = await this.profileRepo.findAndCount({
where: { where: {
isActive: false, keycloak: IsNull(),
citizenId: Like(`%${body.keyword}%`), citizenId: Like(`%${body.keyword}%`),
}, },
relations: ["posType", "posLevel", "current_holders"], relations: ["posType", "posLevel", "current_holders"],
@ -4004,7 +3998,7 @@ export class ProfileEmployeeTempController extends Controller {
case "firstname": case "firstname":
[findProfile, total] = await this.profileRepo.findAndCount({ [findProfile, total] = await this.profileRepo.findAndCount({
where: { where: {
isActive: false, keycloak: IsNull(),
firstName: Like(`%${body.keyword}%`), firstName: Like(`%${body.keyword}%`),
}, },
relations: ["posType", "posLevel", "current_holders"], relations: ["posType", "posLevel", "current_holders"],
@ -4016,7 +4010,7 @@ export class ProfileEmployeeTempController extends Controller {
case "lastname": case "lastname":
[findProfile, total] = await this.profileRepo.findAndCount({ [findProfile, total] = await this.profileRepo.findAndCount({
where: { where: {
isActive: false, keycloak: IsNull(),
lastName: Like(`%${body.keyword}%`), lastName: Like(`%${body.keyword}%`),
}, },
relations: ["posType", "posLevel", "current_holders"], relations: ["posType", "posLevel", "current_holders"],
@ -4028,7 +4022,7 @@ export class ProfileEmployeeTempController extends Controller {
default: default:
[findProfile, total] = await this.profileRepo.findAndCount({ [findProfile, total] = await this.profileRepo.findAndCount({
where: { where: {
isActive: false, keycloak: IsNull(),
}, },
relations: ["posType", "posLevel", "current_holders"], relations: ["posType", "posLevel", "current_holders"],
skip, skip,

View file

@ -6,6 +6,8 @@ import HttpError from "../interfaces/http-error";
import { RequestWithUser } from "../middlewares/user"; import { RequestWithUser } from "../middlewares/user";
import { Profile } from "../entities/Profile"; import { Profile } from "../entities/Profile";
import { ProfileGovernment, UpdateProfileGovernment } from "../entities/ProfileGovernment"; import { ProfileGovernment, UpdateProfileGovernment } from "../entities/ProfileGovernment";
import { Position } from "../entities/Position";
import { PosMaster } from "../entities/PosMaster";
import { import {
calculateAge, calculateAge,
calculateGovAge, calculateGovAge,
@ -13,6 +15,7 @@ import {
setLogDataDiff, setLogDataDiff,
} from "../interfaces/utils"; } from "../interfaces/utils";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { OrgRevision } from "../entities/OrgRevision";
import { In } from "typeorm"; import { In } from "typeorm";
@Route("api/v1/org/profile/government") @Route("api/v1/org/profile/government")
@Tags("ProfileGovernment") @Tags("ProfileGovernment")
@ -20,6 +23,9 @@ import { In } from "typeorm";
export class ProfileGovernmentHistoryController extends Controller { export class ProfileGovernmentHistoryController extends Controller {
private profileRepo = AppDataSource.getRepository(Profile); private profileRepo = AppDataSource.getRepository(Profile);
private govRepo = AppDataSource.getRepository(ProfileGovernment); private govRepo = AppDataSource.getRepository(ProfileGovernment);
private positionRepo = AppDataSource.getRepository(Position);
private posMasterRepo = AppDataSource.getRepository(PosMaster);
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
/** /**
* *
* @summary * @summary
@ -27,6 +33,13 @@ export class ProfileGovernmentHistoryController extends Controller {
*/ */
@Get("user") @Get("user")
public async getGovHistoryUser(@Request() request: { user: Record<string, any> }) { public async getGovHistoryUser(@Request() request: { user: Record<string, any> }) {
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub });
if (!profile) { if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
@ -38,19 +51,78 @@ export class ProfileGovernmentHistoryController extends Controller {
posLevel: true, posLevel: true,
}, },
}); });
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const posMaster = await this.posMasterRepo.findOne({
where: {
// orgRevision: {
// orgRevisionIsCurrent: true,
// orgRevisionIsDraft: false,
// },
orgRevisionId: orgRevision?.id,
current_holderId: profile.id,
},
order: { createdAt: "DESC" },
relations: {
orgRoot: true,
orgChild1: true,
orgChild2: true,
orgChild3: true,
orgChild4: true,
},
});
const position = await this.positionRepo.findOne({
where: {
positionIsSelected: true,
posMaster: {
// orgRevision: {
// orgRevisionIsCurrent: true,
// orgRevisionIsDraft: false,
// },
orgRevisionId: orgRevision?.id,
current_holderId: profile.id,
},
},
order: { createdAt: "DESC" },
relations: {
posExecutive: true,
},
});
// ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const fullNameParts = [
posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name,
posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name,
posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name,
posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name,
posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName,
];
const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n");
let orgShortName = "";
if (posMaster != null) {
if (posMaster.orgChild1Id === null) {
orgShortName = posMaster.orgRoot?.orgRootShortName;
} else if (posMaster.orgChild2Id === null) {
orgShortName = posMaster.orgChild1?.orgChild1ShortName;
} else if (posMaster.orgChild3Id === null) {
orgShortName = posMaster.orgChild2?.orgChild2ShortName;
} else if (posMaster.orgChild4Id === null) {
orgShortName = posMaster.orgChild3?.orgChild3ShortName;
} else {
orgShortName = posMaster.orgChild4?.orgChild4ShortName;
}
}
const data = { const data = {
org: record.org ?? null, //สังกัด org: org, //สังกัด
positionField: record.positionField ?? null, //สายงาน positionField: position == null ? null : position.positionField, //สายงาน
position: record.position, //ตำแหน่ง position: record.position, //ตำแหน่ง
posLevel: record.posLevel == null ? null : record.posLevel.posLevelName, //ระดับ posLevel: record.posLevel == null ? null : record.posLevel.posLevelName, //ระดับ
posMasterNo: record.posMasterNo ?? null, //เลขที่ตำแหน่ง posMasterNo: posMaster == null ? null : `${orgShortName} ${posMaster.posMasterNo}`, //เลขที่ตำแหน่ง
posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท
posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร posExecutive:
positionArea: record.positionArea ?? null, //ด้าน/สาขา position == null || position.posExecutive == null
positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร ? null
: position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร
positionArea: position == null ? null : position.positionArea, //ด้าน/สาขา
positionExecutiveField: position == null ? null : position.positionExecutiveField, //ด้านทางการบริหาร
dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate), dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate),
dateRetireLaw: record.dateRetireLaw ?? null, dateRetireLaw: record.dateRetireLaw ?? null,
// govAge: record.dateStart == null ? null : calculateAge(record.dateStart), // govAge: record.dateStart == null ? null : calculateAge(record.dateStart),
@ -78,6 +150,14 @@ export class ProfileGovernmentHistoryController extends Controller {
if (_workflow == false) if (_workflow == false)
await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId);
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
// ค้นหา profile ก่อน // ค้นหา profile ก่อน
const record = await this.profileRepo.findOne({ const record = await this.profileRepo.findOne({
where: { id: profileId }, where: { id: profileId },
@ -123,10 +203,67 @@ export class ProfileGovernmentHistoryController extends Controller {
// ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ
record.profileSalary = profileWithSalary?.profileSalary || []; record.profileSalary = profileWithSalary?.profileSalary || [];
const posMaster = await this.posMasterRepo.findOne({
where: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
order: { createdAt: "DESC" },
relations: {
orgRoot: true,
orgChild1: true,
orgChild2: true,
orgChild3: true,
orgChild4: true,
},
});
const position = await this.positionRepo.findOne({
where: {
positionIsSelected: true,
posMaster: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
},
order: { createdAt: "DESC" },
relations: {
posExecutive: true,
},
});
// if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const fullNameParts = [
posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name,
posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name,
posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name,
posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name,
posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName,
];
const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n");
let orgShortName = "";
if (posMaster != null) {
if (posMaster.orgChild1Id === null) {
orgShortName = posMaster.orgRoot?.orgRootShortName ?? "";
} else if (posMaster.orgChild2Id === null) {
orgShortName = posMaster.orgChild1?.orgChild1ShortName ?? "";
} else if (posMaster.orgChild3Id === null) {
orgShortName = posMaster.orgChild2?.orgChild2ShortName ?? "";
} else if (posMaster.orgChild4Id === null) {
orgShortName = posMaster.orgChild3?.orgChild3ShortName ?? "";
} else {
orgShortName = posMaster.orgChild4?.orgChild4ShortName ?? "";
}
}
let _OrgLeave: any = []; let _OrgLeave: any = [];
let _profileSalary: any = null; let _profileSalary: any = null;
if (record?.isLeave && record?.profileSalary.length > 0) { if (record?.isLeave && record?.profileSalary.length > 0) {
// _OrgLeave = [
// record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null,
// record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null,
// record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null,
// record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null,
// record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null,
// ];
if (record.leaveType == "RETIRE") { if (record.leaveType == "RETIRE") {
_profileSalary = _profileSalary =
record?.profileSalary.length > 1 record?.profileSalary.length > 1
@ -150,23 +287,26 @@ export class ProfileGovernmentHistoryController extends Controller {
} }
} }
const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n");
// ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว
const data = { const data = {
org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด org: record?.isLeave == false ? org : orgLeave, //สังกัด
positionField: record.positionField ?? null, //สายงาน positionField: position == null ? null : position.positionField, //สายงาน
position: record?.position, //ตำแหน่ง position: record?.position, //ตำแหน่ง
posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ
posMasterNo: posMasterNo:
record?.isLeave == false record?.isLeave == false
? record.posMasterNo ?? null ? posMaster == null
? null
: `${orgShortName} ${posMaster.posMasterNo}`
: _profileSalary != null : _profileSalary != null
? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}`
: null, //เลขที่ตำแหน่ง : null, //เลขที่ตำแหน่ง
posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท
posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร posExecutive:
positionArea: record.positionArea ?? null, //ด้าน/สาขา position == null || position.posExecutive == null
positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร ? null
: position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร
positionArea: position == null ? null : position.positionArea, //ด้าน/สาขา
positionExecutiveField: position == null ? null : position.positionExecutiveField, //ด้านทางการบริหาร
dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate),
dateRetireLaw: record?.dateRetireLaw ?? null, dateRetireLaw: record?.dateRetireLaw ?? null,
// govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart),
@ -184,6 +324,14 @@ export class ProfileGovernmentHistoryController extends Controller {
@Get("admin/{profileId}") @Get("admin/{profileId}")
public async getGovHistoryAdmin(@Path() profileId: string) { public async getGovHistoryAdmin(@Path() profileId: string) {
const orgRevision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: {
orgRevisionIsDraft: false,
orgRevisionIsCurrent: true,
},
});
// ค้นหา profile ก่อน // ค้นหา profile ก่อน
const record = await this.profileRepo.findOne({ const record = await this.profileRepo.findOne({
where: { id: profileId }, where: { id: profileId },
@ -229,10 +377,67 @@ export class ProfileGovernmentHistoryController extends Controller {
// ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ
record.profileSalary = profileWithSalary?.profileSalary || []; record.profileSalary = profileWithSalary?.profileSalary || [];
const posMaster = await this.posMasterRepo.findOne({
where: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
order: { createdAt: "DESC" },
relations: {
orgRoot: true,
orgChild1: true,
orgChild2: true,
orgChild3: true,
orgChild4: true,
},
});
const position = await this.positionRepo.findOne({
where: {
positionIsSelected: true,
posMaster: {
orgRevisionId: orgRevision?.id,
current_holderId: profileId,
},
},
order: { createdAt: "DESC" },
relations: {
posExecutive: true,
},
});
// if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
const fullNameParts = [
posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name,
posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name,
posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name,
posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name,
posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName,
];
const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n");
let orgShortName = "";
if (posMaster != null) {
if (posMaster.orgChild1Id === null) {
orgShortName = posMaster.orgRoot?.orgRootShortName;
} else if (posMaster.orgChild2Id === null) {
orgShortName = posMaster.orgChild1?.orgChild1ShortName;
} else if (posMaster.orgChild3Id === null) {
orgShortName = posMaster.orgChild2?.orgChild2ShortName;
} else if (posMaster.orgChild4Id === null) {
orgShortName = posMaster.orgChild3?.orgChild3ShortName;
} else {
orgShortName = posMaster.orgChild4?.orgChild4ShortName;
}
}
let _OrgLeave: any = []; let _OrgLeave: any = [];
let _profileSalary: any = null; let _profileSalary: any = null;
if (record?.isLeave && record?.profileSalary.length > 0) { if (record?.isLeave && record?.profileSalary.length > 0) {
// _OrgLeave = [
// record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null,
// record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null,
// record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null,
// record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null,
// record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null,
// ];
if (record.leaveType == "RETIRE") { if (record.leaveType == "RETIRE") {
_profileSalary = _profileSalary =
record?.profileSalary.length > 1 record?.profileSalary.length > 1
@ -256,23 +461,26 @@ export class ProfileGovernmentHistoryController extends Controller {
} }
} }
const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n");
// ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว
const data = { const data = {
org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด org: record?.isLeave == false ? org : orgLeave, //สังกัด
positionField: record.positionField ?? null, //สายงาน positionField: position == null ? null : position.positionField, //สายงาน
position: record?.position, //ตำแหน่ง position: record?.position, //ตำแหน่ง
posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ
posMasterNo: posMasterNo:
record?.isLeave == false record?.isLeave == false
? record.posMasterNo ?? null ? posMaster == null
? null
: `${orgShortName} ${posMaster.posMasterNo}`
: _profileSalary != null : _profileSalary != null
? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}`
: null, //เลขที่ตำแหน่ง : null, //เลขที่ตำแหน่ง
posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท
posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร posExecutive:
positionArea: record.positionArea ?? null, //ด้าน/สาขา position == null || position.posExecutive == null
positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร ? null
: position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร
positionArea: position == null ? null : position.positionArea, //ด้าน/สาขา
positionExecutiveField: position == null ? null : position.positionExecutiveField, //ด้านทางการบริหาร
dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate),
dateRetireLaw: record?.dateRetireLaw ?? null, dateRetireLaw: record?.dateRetireLaw ?? null,
// govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart),
@ -371,4 +579,3 @@ export class ProfileGovernmentHistoryController extends Controller {
return new HttpSuccess(); return new HttpSuccess();
} }
} }

View file

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

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,6 @@ import { Profile } from "../entities/Profile";
import { In, LessThan, IsNull, MoreThan } from "typeorm"; import { In, LessThan, IsNull, MoreThan } from "typeorm";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils"; import { setLogDataDiff } from "../interfaces/utils";
import { normalizeDurationSumSimple } from "../utils/tenure";
import { Command } from "../entities/Command"; import { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot"; import { OrgRoot } from "../entities/OrgRoot";
import Extension from "../interfaces/extension"; import Extension from "../interfaces/extension";
@ -161,14 +160,6 @@ export class ProfileSalaryEmployeeController extends Controller {
_position.length > 1 _position.length > 1
? _position.slice(1).map((curr: any, index: number) => ({ ? _position.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0, 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, name: _position[index]?.positionName,
})) }))
: []; : [];
@ -179,25 +170,14 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) { if (existing) {
existing.days += curr.days; existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else { } else {
existing = { existing = { name: curr.name, days: curr.days };
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing); acc.push(existing);
} }
// Normalize the summed values using calendar arithmetic existing.year = Math.floor(existing.days / 365.2524);
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.year = normalized.years; existing.day = Math.floor(existing.days % 30.4375);
existing.month = normalized.months;
existing.day = normalized.days;
return acc; return acc;
}, },
@ -213,14 +193,6 @@ export class ProfileSalaryEmployeeController extends Controller {
_posLevel.length > 1 _posLevel.length > 1
? _posLevel.slice(1).map((curr: any, index: number) => ({ ? _posLevel.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0, 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: name:
!_posLevel[index]?.positionType && _posLevel[index]?.positionCee !_posLevel[index]?.positionType && _posLevel[index]?.positionCee
? `ระดับ ${_posLevel[index]?.positionCee.trim()}` ? `ระดับ ${_posLevel[index]?.positionCee.trim()}`
@ -234,25 +206,14 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) { if (existing) {
existing.days += curr.days; existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else { } else {
existing = { existing = { name: curr.name, days: curr.days };
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing); acc.push(existing);
} }
// Normalize the summed values using calendar arithmetic existing.year = Math.floor(existing.days / 365.2524);
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.year = normalized.years; existing.day = Math.floor(existing.days % 30.4375);
existing.month = normalized.months;
existing.day = normalized.days;
return acc; return acc;
}, },
@ -290,14 +251,6 @@ export class ProfileSalaryEmployeeController extends Controller {
_position.length > 1 _position.length > 1
? _position.slice(1).map((curr: any, index: number) => ({ ? _position.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0, 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, name: _position[index]?.positionName,
})) }))
: []; : [];
@ -308,25 +261,14 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) { if (existing) {
existing.days += curr.days; existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else { } else {
existing = { existing = { name: curr.name, days: curr.days };
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing); acc.push(existing);
} }
// Normalize the summed values using calendar arithmetic existing.year = Math.floor(existing.days / 365.2524);
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.year = normalized.years; existing.day = Math.floor(existing.days % 30.4375);
existing.month = normalized.months;
existing.day = normalized.days;
return acc; return acc;
}, },
@ -342,14 +284,6 @@ export class ProfileSalaryEmployeeController extends Controller {
_posLevel.length > 1 _posLevel.length > 1
? _posLevel.slice(1).map((curr: any, index: number) => ({ ? _posLevel.slice(1).map((curr: any, index: number) => ({
days: curr.days_diff ? Number(curr.days_diff) : 0, 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: name:
!_posLevel[index]?.positionType && _posLevel[index]?.positionCee !_posLevel[index]?.positionType && _posLevel[index]?.positionCee
? `ระดับ ${_posLevel[index]?.positionCee.trim()}` ? `ระดับ ${_posLevel[index]?.positionCee.trim()}`
@ -363,25 +297,14 @@ export class ProfileSalaryEmployeeController extends Controller {
if (existing) { if (existing) {
existing.days += curr.days; existing.days += curr.days;
existing.year += curr.year;
existing.month += curr.month;
existing.day += curr.day;
} else { } else {
existing = { existing = { name: curr.name, days: curr.days };
name: curr.name,
days: curr.days,
year: curr.year,
month: curr.month,
day: curr.day,
};
acc.push(existing); acc.push(existing);
} }
// Normalize the summed values using calendar arithmetic existing.year = Math.floor(existing.days / 365.2524);
const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); existing.month = Math.floor((existing.days / 30.4375) % 12);
existing.year = normalized.years; existing.day = Math.floor(existing.days % 30.4375);
existing.month = normalized.months;
existing.day = normalized.days;
return acc; return acc;
}, },
@ -475,17 +398,6 @@ export class ProfileSalaryEmployeeController extends Controller {
else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง";
} }
Object.assign(data, { ...body, ...meta }); Object.assign(data, { ...body, ...meta });
// 12,15,16 isGovernment = false & dateGovernment = commandDateAffect
if (["12", "15", "16"].includes(body.commandCode ?? "")) {
data.isGovernment = false;
if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
data.isGovernment = true;
if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect;
}
const history = new ProfileSalaryHistory(); const history = new ProfileSalaryHistory();
Object.assign(history, { ...data, id: undefined }); Object.assign(history, { ...data, id: undefined });
const _null: any = null; const _null: any = null;
@ -620,16 +532,6 @@ export class ProfileSalaryEmployeeController extends Controller {
else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง";
} }
Object.assign(record, body); Object.assign(record, body);
// 12,15,16 isGovernment = false & dateGovernment = commandDateAffect
if (["12", "15", "16"].includes(body.commandCode ?? "")) {
record.isGovernment = false;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null;
}
// 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect
else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) {
record.isGovernment = true;
if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null;
}
Object.assign(history, { ...record, id: undefined }); Object.assign(history, { ...record, id: undefined });
history.profileSalaryId = salaryId; history.profileSalaryId = salaryId;

View file

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

@ -23,7 +23,6 @@ import HttpError from "../interfaces/http-error";
import { ProfileTrainingHistory } from "../entities/ProfileTrainingHistory"; import { ProfileTrainingHistory } from "../entities/ProfileTrainingHistory";
import { RequestWithUser } from "../middlewares/user"; import { RequestWithUser } from "../middlewares/user";
import { Profile } from "../entities/Profile"; import { Profile } from "../entities/Profile";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils"; import { setLogDataDiff } from "../interfaces/utils";
import { ProfileDevelopment } from "../entities/ProfileDevelopment"; import { ProfileDevelopment } from "../entities/ProfileDevelopment";
@ -34,13 +33,11 @@ import { In } from "typeorm";
@Security("bearerAuth") @Security("bearerAuth")
export class ProfileTrainingController extends Controller { export class ProfileTrainingController extends Controller {
private profileRepo = AppDataSource.getRepository(Profile); private profileRepo = AppDataSource.getRepository(Profile);
private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
private trainingRepo = AppDataSource.getRepository(ProfileTraining); private trainingRepo = AppDataSource.getRepository(ProfileTraining);
private trainingHistoryRepo = AppDataSource.getRepository(ProfileTrainingHistory); private trainingHistoryRepo = AppDataSource.getRepository(ProfileTrainingHistory);
private developmentRepo = AppDataSource.getRepository(ProfileDevelopment); private developmentRepo = AppDataSource.getRepository(ProfileDevelopment);
private developmentHistoryRepo = AppDataSource.getRepository(ProfileDevelopmentHistory); private developmentHistoryRepo = AppDataSource.getRepository(ProfileDevelopmentHistory);
@Get("user") @Get("user")
public async getTrainingUser(@Request() request: { user: Record<string, any> }) { public async getTrainingUser(@Request() request: { user: Record<string, any> }) {
const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub });
@ -259,83 +256,4 @@ export class ProfileTrainingController extends Controller {
return new HttpSuccess(); return new HttpSuccess();
} }
/**
* API / IDP
* @summary API / IDP
*/
@Post("delete-byId")
public async deleteById(
@Body() reqBody: {
type: string;
profileId: string;
developmentId: string;
},
@Request() req: RequestWithUser
) {
const type = reqBody.type?.trim().toUpperCase();
// 1. validate profile
if (type === "OFFICER") {
const profile = await this.profileRepo.findOneBy({ id: reqBody.profileId });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
} else {
const profile = await this.profileEmployeeRepo.findOneBy({ id: reqBody.profileId });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
}
const profileField = type === "OFFICER" ? "profileId" : "profileEmployeeId";
// 2. หา ProfileTraining
const trainings = await this.trainingRepo.find({
select: { id: true },
where: {
developmentId: reqBody.developmentId,
[profileField]: reqBody.profileId,
},
});
if (trainings.length > 0) {
const trainingIds = trainings.map(x => x.id);
// 3. ลบ TrainingHistory
await this.trainingHistoryRepo.delete({
profileTrainingId: In(trainingIds),
});
// 4. ลบ ProfileTraining
await this.trainingRepo.delete({
id: In(trainingIds),
});
}
// 5. หา ProfileDevelopment
const developments = await this.developmentRepo.find({
select: { id: true },
where: {
kpiDevelopmentId: reqBody.developmentId,
[profileField]: reqBody.profileId,
},
});
if (developments.length > 0) {
const devIds = developments.map(x => x.id);
// 6. ลบ DevelopmentHistory
await this.developmentHistoryRepo.delete({
profileDevelopmentId: In(devIds),
});
// 7. ลบ ProfileDevelopment
await this.developmentRepo.delete({
id: In(devIds),
});
}
return new HttpSuccess();
}
} }

View file

@ -38,10 +38,6 @@ export class ScriptProfileOrgController extends Controller {
process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24", process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24",
10, 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 * Script to update profile's organizational structure in leave service and sync to Keycloak
@ -49,7 +45,7 @@ export class ScriptProfileOrgController extends Controller {
* @summary Update org structure for profiles updated within a certain time window and sync to Keycloak * @summary Update org structure for profiles updated within a certain time window and sync to Keycloak
*/ */
@Post("update-org") @Post("update-org")
public async cronjobUpdateOrg(@Request() _request: RequestWithUser) { public async cronjobUpdateOrg(@Request() request: RequestWithUser) {
// Idempotency check - prevent concurrent runs // Idempotency check - prevent concurrent runs
if (this.isRunning) { if (this.isRunning) {
console.log("cronjobUpdateOrg: Job already running, skipping this execution"); console.log("cronjobUpdateOrg: Job already running, skipping this execution");
@ -180,6 +176,21 @@ 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 // Group profile IDs by type for proper syncing
const profileIdsByType = this.groupProfileIdsByType(payloads); const profileIdsByType = this.groupProfileIdsByType(payloads);
@ -245,90 +256,16 @@ export class ScriptProfileOrgController extends Controller {
syncResults.failed += typeResult.failed; 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; const duration = Date.now() - startTime;
console.log("cronjobUpdateOrg: Job completed", { console.log("cronjobUpdateOrg: Job completed", {
duration: `${duration}ms`, duration: `${duration}ms`,
processed: payloads.length, processed: payloads.length,
leaveServiceResults,
syncResults, syncResults,
}); });
return new HttpSuccess({ return new HttpSuccess({
message: "Update org completed", message: "Update org completed",
processed: payloads.length, processed: payloads.length,
leaveServiceResults,
syncResults, syncResults,
duration: `${duration}ms`, duration: `${duration}ms`,
}); });

View file

@ -1,6 +1,5 @@
import { Body, Controller, Post, Request, Route, Security } from "tsoa"; import { Body, Controller, Post, Route } from "tsoa";
import { sendWebSocket } from "../services/webSocket"; import { sendWebSocket } from "../services/webSocket";
import { RequestWithUser } from "../middlewares/user";
@Route("/api/v1/org/through-socket") @Route("/api/v1/org/through-socket")
export class SocketController extends Controller { export class SocketController extends Controller {
@ -23,39 +22,4 @@ 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

@ -137,7 +137,6 @@ export class KeycloakController extends Controller {
profile.keycloak = userId; profile.keycloak = userId;
} }
profile.email = body.email == null ? _null : body.email; profile.email = body.email == null ? _null : body.email;
profile.isDelete = false;
await this.profileRepo.save(profile); await this.profileRepo.save(profile);
// Update Keycloak with profile prefix after profile is loaded // Update Keycloak with profile prefix after profile is loaded
@ -203,7 +202,6 @@ export class KeycloakController extends Controller {
profile.keycloak = userId; profile.keycloak = userId;
} }
profile.email = body.email == null ? _null : body.email; profile.email = body.email == null ? _null : body.email;
profile.isDelete = false;
await this.profileEmpRepo.save(profile); await this.profileEmpRepo.save(profile);
// Update Keycloak with profile prefix after profile is loaded // Update Keycloak with profile prefix after profile is loaded
await updateUserAttributes(userId, { await updateUserAttributes(userId, {
@ -276,18 +274,14 @@ export class KeycloakController extends Controller {
}); });
if (!profileEmp) { if (!profileEmp) {
} else { } else {
// Task #228 const _null: any = null;
// const _null: any = null; profileEmp.keycloak = _null;
// profileEmp.keycloak = _null;
profileEmp.isDelete = true;
profileEmp.roleKeycloaks = []; profileEmp.roleKeycloaks = [];
await this.profileEmpRepo.save(profileEmp); await this.profileEmpRepo.save(profileEmp);
} }
} else { } else {
// Task #228 const _null: any = null;
// const _null: any = null; profile.keycloak = _null;
// profile.keycloak = _null;
profile.isDelete = true;
profile.roleKeycloaks = []; profile.roleKeycloaks = [];
await this.profileRepo.save(profile); await this.profileRepo.save(profile);
return new HttpSuccess(); return new HttpSuccess();
@ -573,34 +567,24 @@ export class KeycloakController extends Controller {
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
.where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''")
.andWhere("profile.isDelete = :isDelete", { isDelete: false })
.andWhere(checkChildFromRole) .andWhere(checkChildFromRole)
.andWhere(conditions) .andWhere(conditions)
.andWhere( .andWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.orWhere( qb.orWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
? `profile.citizenId LIKE :keyword` ? `profile.citizenId like '%${body.keyword}%'`
: "1=1", : "1=1",
{
keyword: `%${body.keyword}%`,
}
) )
.orWhere( .orWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
? `profile.email LIKE :keyword` ? `profile.email like '%${body.keyword}%'`
: "1=1", : "1=1",
{
keyword: `%${body.keyword}%`,
}
) )
.orWhere( .orWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) LIKE :keyword` ? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) like '%${body.keyword}%'`
: "1=1", : "1=1",
{
keyword: `%${body.keyword}%`,
}
); );
}), }),
) )
@ -626,7 +610,6 @@ export class KeycloakController extends Controller {
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
.where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''")
.andWhere("profileEmployee.isDelete = :isDelete", { isDelete: false })
.andWhere(checkChildFromRole) .andWhere(checkChildFromRole)
.andWhere(conditions) .andWhere(conditions)
.andWhere({ employeeClass: "PERM" }) .andWhere({ employeeClass: "PERM" })
@ -634,27 +617,18 @@ export class KeycloakController extends Controller {
new Brackets((qb) => { new Brackets((qb) => {
qb.orWhere( qb.orWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
? `profileEmployee.citizenId LIKE :keyword` ? `profileEmployee.citizenId like '%${body.keyword}%'`
: "1=1", : "1=1",
{
keyword: `%${body.keyword}%`,
}
) )
.orWhere( .orWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
? `profileEmployee.email LIKE :keyword` ? `profileEmployee.email like '%${body.keyword}%'`
: "1=1", : "1=1",
{
keyword: `%${body.keyword}%`,
}
) )
.orWhere( .orWhere(
body.keyword != null && body.keyword != "" body.keyword != null && body.keyword != ""
? `CONCAT(profileEmployee.prefix, profileEmployee.firstName," ",profileEmployee.lastName) LIKE :keyword` ? `CONCAT(profileEmployee.prefix, profileEmployee.firstName," ",profileEmployee.lastName) like '%${body.keyword}%'`
: "1=1", : "1=1",
{
keyword: `%${body.keyword}%`,
}
); );
}), }),
) )
@ -780,7 +754,6 @@ export class KeycloakController extends Controller {
profile.keycloak = userId; profile.keycloak = userId;
} }
profile.email = body.email == null ? _null : body.email; profile.email = body.email == null ? _null : body.email;
profile.isDelete = false;
await this.profileEmpRepo.save(profile); await this.profileEmpRepo.save(profile);
// Update Keycloak with profile prefix after profile is loaded // Update Keycloak with profile prefix after profile is loaded
await updateUserAttributes(userId, { await updateUserAttributes(userId, {
@ -832,68 +805,6 @@ export class KeycloakController extends Controller {
if (!result) throw new Error("Failed. Cannot remove group to user."); if (!result) throw new Error("Failed. Cannot remove group to user.");
} }
@Post("user/reset-password")
@Security("bearerAuth", ["admin"])
async resetUserPassword(@Request() req: RequestWithUser, @Body() body: { keycloak: string }) {
if (!req.user.role.includes("ADMIN") && !req.user.role.includes("SUPER_ADMIN")) {
throw new HttpError(HttpStatus.FORBIDDEN, "ไม่มีสิทธิ์ดำเนินการ");
}
let profile: Profile | ProfileEmployee | null = await this.profileRepo.findOne({
where: { keycloak: body.keycloak },
select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"],
});
let isEmployee = false;
if (!profile) {
profile = await this.profileEmpRepo.findOne({
where: { keycloak: body.keycloak, employeeClass: "PERM" },
select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"],
});
isEmployee = true;
}
if (!profile) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้");
}
if (!profile.keycloak) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ผู้ใช้ไม่ได้เชื่อมต่อกับ Keycloak");
}
let newPassword: string;
const isProduction = process.env.NODE_ENV === "production";
if (isProduction && profile.birthDate) {
const _date = new Date(profile.birthDate.toDateString())
.getDate()
.toString()
.padStart(2, "0");
const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1)
.toString()
.padStart(2, "0");
const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543;
newPassword = `${_date}${_month}${_year}`;
} else {
newPassword = "P@ssw0rd";
}
const result = await changeUserPassword(profile.keycloak, newPassword);
if (!result) {
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "ไม่สามารถรีเซ็ตรหัสผ่านได้");
}
addLogSequence(req, {
action: "reset-password",
status: "success",
description: `รีเซ็ตรหัสผ่านสำหรับ ${profile.firstName} ${profile.lastName} (${profile.citizenId})`,
});
const response = new HttpSuccess();
response.message = "รีเซ็ตรหัสผ่านสำเร็จ";
return response;
}
@Get("user/role/{id}") @Get("user/role/{id}")
async getRoleUser(@Request() req: RequestWithUser, @Path("id") id: string) { async getRoleUser(@Request() req: RequestWithUser, @Path("id") id: string) {
const profile = await this.profileRepo.findOne({ const profile = await this.profileRepo.findOne({

View file

@ -22,8 +22,7 @@ import { viewDirectorActing } from "../entities/view/viewDirectorActing";
import { viewDirector } from "../entities/view/viewDirector"; import { viewDirector } from "../entities/view/viewDirector";
import { ProfileEmployee } from "../entities/ProfileEmployee"; import { ProfileEmployee } from "../entities/ProfileEmployee";
import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { OrgRoot } from "../entities/OrgRoot";
import { getPosMasterPositions } from "../services/PositionService";
@Route("api/v1/org/workflow") @Route("api/v1/org/workflow")
@Tags("Workflow") @Tags("Workflow")
@Security("bearerAuth") @Security("bearerAuth")
@ -35,7 +34,7 @@ export class WorkflowController extends Controller {
private stateUserCommentRepo = AppDataSource.getRepository(StateUserComment); private stateUserCommentRepo = AppDataSource.getRepository(StateUserComment);
private profileRepo = AppDataSource.getRepository(Profile); private profileRepo = AppDataSource.getRepository(Profile);
private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
private orgRootRepo = AppDataSource.getRepository(OrgRoot);
private metaWorkflowRepo = AppDataSource.getRepository(MetaWorkflow); private metaWorkflowRepo = AppDataSource.getRepository(MetaWorkflow);
private metaStateRepo = AppDataSource.getRepository(MetaState); private metaStateRepo = AppDataSource.getRepository(MetaState);
private metaStateOperatorRepo = AppDataSource.getRepository(MetaStateOperator); private metaStateOperatorRepo = AppDataSource.getRepository(MetaStateOperator);
@ -55,7 +54,6 @@ export class WorkflowController extends Controller {
posTypeName: string; posTypeName: string;
fullName?: string | null; fullName?: string | null;
isDeputy?: boolean | null; isDeputy?: boolean | null;
orgRootId?: string | null;
}, },
) { ) {
// ขั้นที่ 1: ทำการค้นหา profile และ metaWorkflow แบบ parallel // ขั้นที่ 1: ทำการค้นหา profile และ metaWorkflow แบบ parallel
@ -205,10 +203,9 @@ export class WorkflowController extends Controller {
posMasterAssigns: { assignId: body.sysName }, posMasterAssigns: { assignId: body.sysName },
orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true },
current_holderId: Not(IsNull()), // เพิ่มเงื่อนไขนี้เพื่อกรองเฉพาะที่มี current_holder current_holderId: Not(IsNull()), // เพิ่มเงื่อนไขนี้เพื่อกรองเฉพาะที่มี current_holder
...(body.orgRootId && { orgRootId: body.orgRootId }), // กรองเฉพาะที่อยู่ในสำนักเดียวกัน (ถ้าส่งมา)
}, },
relations: ["orgChild1"], relations: ["orgChild1"],
// select: ["current_holderId", "orgChild1"], // เลือกเฉพาะ field ที่จำเป็น select: ["current_holderId", "orgChild1"], // เลือกเฉพาะ field ที่จำเป็น
}); });
// สร้าง StateOperatorUsers สำหรับ officers // สร้าง StateOperatorUsers สำหรับ officers
@ -238,21 +235,11 @@ export class WorkflowController extends Controller {
savedStates.find((state) => state.id === so.stateId && state.order === 1), savedStates.find((state) => state.id === so.stateId && state.order === 1),
); );
// add link sysName = REGISTRY_PROFILE or REGISTRY_PROFILE_EMP
let notiLink = "";
if (body.sysName === "REGISTRY_PROFILE") {
notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`;
} else if (body.sysName === "REGISTRY_PROFILE_EMP") {
notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`;
} else if (body.sysName === "REGISTRY_IDP") {
notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`;
}
const notificationReceivers = stateOperatorUsersToCreate const notificationReceivers = stateOperatorUsersToCreate
.filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) .filter((user) => firstStateOperators.some((op) => op.operator === user.operator))
.map((user) => ({ .map((user) => ({
receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId,
notiLink: notiLink, notiLink: "",
})); }));
// ส่ง notification แบบ fire-and-forget // ส่ง notification แบบ fire-and-forget
@ -911,20 +898,6 @@ export class WorkflowController extends Controller {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบตำแหน่งผู้ใช้งาน"); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบตำแหน่งผู้ใช้งาน");
} }
// Task #2342 list ปลัด และรองปลัดใน popup ผู้บังคับบัญชา และผู้มีอำนาจเพิ่ม
const roodIds = [posMasterUser.orgRootId];
const orgRoot = await this.orgRootRepo.findOne({
select: { id: true, isDeputy: true },
where: {
id: Not(posMasterUser.orgRootId),
isDeputy: true,
orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
},
});
if (orgRoot && orgRoot.isDeputy) {
roodIds.push(orgRoot.id);
}
// 2. Pre-calculate conditions - ย้ายออกมาข้างนอก // 2. Pre-calculate conditions - ย้ายออกมาข้างนอก
const posType = posMasterUser.current_holder?.posType?.posTypeName; const posType = posMasterUser.current_holder?.posType?.posTypeName;
const posLevel = posMasterUser.current_holder?.posLevel?.posLevelName; const posLevel = posMasterUser.current_holder?.posLevel?.posLevelName;
@ -954,23 +927,23 @@ export class WorkflowController extends Controller {
if (type.trim().toUpperCase() === "OPERATE" || body.type === "employee") { if (type.trim().toUpperCase() === "OPERATE" || body.type === "employee") {
mainConditions = [ mainConditions = [
{ ...baseCondition, orgRootId: In(roodIds), orgChild1Id: IsNull() }, { ...baseCondition, orgRootId: posMasterUser.orgRootId, orgChild1Id: IsNull() },
{ {
...baseCondition, ...baseCondition,
orgRootId: In(roodIds), orgRootId: posMasterUser.orgRootId,
orgChild1Id: posMasterUser.orgChild1Id, orgChild1Id: posMasterUser.orgChild1Id,
orgChild2Id: IsNull(), orgChild2Id: IsNull(),
}, },
{ {
...baseCondition, ...baseCondition,
orgRootId: In(roodIds), orgRootId: posMasterUser.orgRootId,
orgChild1Id: posMasterUser.orgChild1Id, orgChild1Id: posMasterUser.orgChild1Id,
orgChild2Id: posMasterUser.orgChild2Id, orgChild2Id: posMasterUser.orgChild2Id,
orgChild3Id: IsNull(), orgChild3Id: IsNull(),
}, },
{ {
...baseCondition, ...baseCondition,
orgRootId: In(roodIds), orgRootId: posMasterUser.orgRootId,
orgChild1Id: posMasterUser.orgChild1Id, orgChild1Id: posMasterUser.orgChild1Id,
orgChild2Id: posMasterUser.orgChild2Id, orgChild2Id: posMasterUser.orgChild2Id,
orgChild3Id: posMasterUser.orgChild3Id, orgChild3Id: posMasterUser.orgChild3Id,
@ -978,7 +951,7 @@ export class WorkflowController extends Controller {
}, },
{ {
...baseCondition, ...baseCondition,
orgRootId: In(roodIds), orgRootId: posMasterUser.orgRootId,
orgChild1Id: posMasterUser.orgChild1Id, orgChild1Id: posMasterUser.orgChild1Id,
orgChild2Id: posMasterUser.orgChild2Id, orgChild2Id: posMasterUser.orgChild2Id,
orgChild3Id: posMasterUser.orgChild3Id, orgChild3Id: posMasterUser.orgChild3Id,
@ -989,7 +962,7 @@ export class WorkflowController extends Controller {
mainConditions = [ mainConditions = [
{ {
...baseCondition, ...baseCondition,
orgRootId: In(roodIds), orgRootId: posMasterUser.orgRootId,
orgChild1Id: IsNull(), orgChild1Id: IsNull(),
orgChild2Id: IsNull(), orgChild2Id: IsNull(),
orgChild3Id: IsNull(), orgChild3Id: IsNull(),
@ -1008,7 +981,7 @@ export class WorkflowController extends Controller {
}, },
]; ];
} else { } else {
mainConditions = [{ ...baseCondition, orgRootId: In(roodIds) }]; mainConditions = [{ ...baseCondition, orgRootId: posMasterUser.orgRootId }];
} }
// 4. สร้าง optimized query builder // 4. สร้าง optimized query builder
@ -1072,48 +1045,12 @@ export class WorkflowController extends Controller {
]); ]);
// 8. ปรับ response mapping (ถ้าจำเป็น) // 8. ปรับ response mapping (ถ้าจำเป็น)
let posMasterPositionMap: Map<string, string> = new Map(); const processedData = data.map((x: any) => ({
if (body.isAct) {
// ดึง posMasterId ทั้งหมด (36 ตัวแรกของ key) เพื่อ query positionName
const posMasterIds = data
.map((x) => x.key?.substring(0, 36))
.filter((id) => id && id.length === 36);
posMasterPositionMap = await getPosMasterPositions(posMasterIds);
}
const processedData = data.map((x: any) => {
let newPositionSign = x.positionSign;
if (body.isAct) {
// ตำแหน่งของคนที่เลือกไปรักษาการ
let childPosition = "";
if (x.positionSignChild) {
childPosition = x.positionSignChild;
} else if (x.posExecutiveName) {
childPosition = x.posExecutiveName;
} else {
childPosition = `${x.position || ""}${x.posLevel || ""}`.trim();
}
// ตำแหน่งที่รักษาการแทน
const posMasterId = x.key?.substring(0, 36);
const targetPosition = x.positionSign
? x.positionSign
: posMasterPositionMap.get(posMasterId) || "";
// สร้าง positionSign ใหม่
newPositionSign = `${childPosition} รักษาการในตำแหน่ง${targetPosition}`;
}
return {
...x, ...x,
positionSign: newPositionSign,
posExecutiveNameOrg: posExecutiveNameOrg:
(x.posExecutiveName ?? "") + (x.posExecutiveName ?? "") +
(x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""), (x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""),
}; }));
});
return new HttpSuccess({ data: processedData, total }); return new HttpSuccess({ data: processedData, total });
} }

View file

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

View file

@ -38,11 +38,11 @@ export class Issues extends EntityBase {
@Column({ @Column({
type: "enum", type: "enum",
enum: ["NEW", "IN_PROGRESS", "RESOLVED", "CLOSED", "HELPDESK_IN_PROGRESS", "REPLIED"], enum: ["NEW", "IN_PROGRESS", "RESOLVED", "CLOSED"],
default: "NEW", default: "NEW",
comment: "สถานะการแก้ไขปัญหา", comment: "สถานะการแก้ไขปัญหา",
}) })
status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED";
@BeforeInsert() @BeforeInsert()
async generateCodeIssue() { async generateCodeIssue() {
@ -77,7 +77,7 @@ export interface IssueResponse {
menu: string | null; menu: string | null;
org: string | null; org: string | null;
remark: string | null; remark: string | null;
status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED";
createdAt: Date; createdAt: Date;
lastUpdatedAt: Date; lastUpdatedAt: Date;
createdFullName: string; createdFullName: string;
@ -90,7 +90,7 @@ export interface CreateIssueRequest {
title: string; title: string;
description?: string; description?: string;
system: string; system: string;
status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED";
menu?: string; menu?: string;
org?: string; org?: string;
email?: string; email?: string;
@ -98,6 +98,6 @@ export interface CreateIssueRequest {
} }
export interface UpdateIssueRequest { export interface UpdateIssueRequest {
status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED";
remark?: string; remark?: string;
} }

View file

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

View file

@ -50,7 +50,6 @@ import { ProfileAssistance } from "./ProfileAssistance";
import { ProfileSalaryTemp } from "./ProfileSalaryTemp"; import { ProfileSalaryTemp } from "./ProfileSalaryTemp";
import { PositionSalaryEditHistory } from "./PositionSalaryEditHistory"; import { PositionSalaryEditHistory } from "./PositionSalaryEditHistory";
import { ProfileChangeName } from "./ProfileChangeName"; import { ProfileChangeName } from "./ProfileChangeName";
import { ProfileAbsentLate } from "./ProfileAbsentLate";
@Entity("profile") @Entity("profile")
export class Profile extends EntityBase { export class Profile extends EntityBase {
@ -140,54 +139,6 @@ export class Profile extends EntityBase {
}) })
posTypeId: string | null; posTypeId: string | null;
@Column({
nullable: true,
comment: "สายงาน",
length: 45,
default: null,
})
positionField: string;
@Column({
nullable: true,
comment: "ตำแหน่งทางการบริหาร",
length: 255,
default: null,
})
posExecutive?: string;
@Column({
nullable: true,
comment: "ด้าน/สาขา",
length: 255,
default: null,
})
positionArea?: string;
@Column({
nullable: true,
comment: "ด้านทางการบริหาร",
length: 255,
default: null,
})
positionExecutiveField?: string;
@Column({
nullable: true,
comment: "เลขที่ตำแหน่ง",
length: 255,
default: null,
})
posMasterNo?: string;
@Column({
nullable: true,
comment: "สังกัด",
type: "text",
default: null,
})
org?: string;
@Column({ @Column({
nullable: true, nullable: true,
length: 255, length: 255,
@ -236,12 +187,6 @@ export class Profile extends EntityBase {
}) })
keycloak: string; keycloak: string;
@Column({
comment: "สถานะการถูกลบผู้ใช้งานใน keycloak",
default: false,
})
isDelete: boolean;
@Column({ @Column({
comment: "ทดลองปฏิบัติหน้าที่", comment: "ทดลองปฏิบัติหน้าที่",
default: false, default: false,
@ -601,9 +546,6 @@ export class Profile extends EntityBase {
@OneToMany(() => ProfileChangeName, (v) => v.profile) @OneToMany(() => ProfileChangeName, (v) => v.profile)
profileChangeNames: ProfileChangeName[]; profileChangeNames: ProfileChangeName[];
@OneToMany(() => ProfileAbsentLate, (v) => v.profile)
profileAbsentLates: ProfileAbsentLate[];
@ManyToOne(() => PosLevel, (posLevel) => posLevel.profiles) @ManyToOne(() => PosLevel, (posLevel) => posLevel.profiles)
@JoinColumn({ name: "posLevelId" }) @JoinColumn({ name: "posLevelId" })
posLevel: PosLevel; posLevel: PosLevel;

View file

@ -1,98 +0,0 @@
import { Entity, Column, ManyToOne, JoinColumn } from "typeorm";
import { EntityBase } from "./base/Base";
import { Profile } from "./Profile";
// Enums
export enum AbsentLateStatus {
LATE = "LATE", // มาสาย
ABSENT = "ABSENT", // ขาดราชการ
}
export enum StampType {
FULL_DAY = "FULL_DAY", // เต็มวัน
MORNING = "MORNING", // ครึ่งเช้า
AFTERNOON = "AFTERNOON", // ครึ่งบ่าย
}
@Entity("profileAbsentLate")
export class ProfileAbsentLate extends EntityBase {
@Column({
nullable: true,
length: 40,
comment: "คีย์นอก(FK)ของตาราง Profile",
default: null,
})
profileId: string;
@Column({
type: "enum",
enum: AbsentLateStatus,
comment: "สถานะ มาสาย/ขาดราชการ",
nullable: false,
})
status: AbsentLateStatus;
@Column({
type: "datetime",
comment: "วันที่และเวลาที่ลงเวลา",
nullable: false,
})
stampDate: Date;
@Column({
type: "enum",
enum: StampType,
comment: "เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย",
default: StampType.FULL_DAY,
})
stampType: StampType;
@Column({
type: "decimal",
precision: 2,
scale: 1,
comment: "จำนวน (1.0/0.5)",
default: "1.0",
})
stampAmount: number;
@Column({
type: "varchar",
length: 250,
comment: "หมายเหตุ",
nullable: true,
})
remark: string;
@Column({
comment: "สถานะลบข้อมูล",
default: false,
})
isDeleted: boolean;
@ManyToOne(() => Profile, (profile) => profile.profileAbsentLates)
@JoinColumn({ name: "profileId" })
profile: Profile;
}
// DTO Classes
export class CreateProfileAbsentLate {
profileId: string;
status: AbsentLateStatus;
stampDate: Date;
stampType?: StampType;
stampAmount?: number;
remark?: string;
}
export class CreateProfileAbsentLateBatch {
records: CreateProfileAbsentLate[];
}
export type UpdateProfileAbsentLate = {
status?: AbsentLateStatus;
stampDate?: Date;
stampType?: StampType;
stampAmount?: number;
remark?: string;
};

View file

@ -1,20 +0,0 @@
import { Entity, Column } from "typeorm";
import {
ProfileAbsentLate,
AbsentLateStatus,
StampType,
} from "./ProfileAbsentLate";
@Entity("profileAbsentLateHistory")
export class ProfileAbsentLateHistory extends ProfileAbsentLate {
@Column({
nullable: true,
length: 40,
comment: "คีย์นอก(FK)ของตาราง ProfileAbsentLate",
default: null,
})
profileAbsentLateId: string;
}
// Export enums for re-use
export { AbsentLateStatus, StampType };

View file

@ -38,7 +38,6 @@ import { StateOperatorUser } from "./StateOperatorUser";
import { EmployeeTempPosMaster } from "./EmployeeTempPosMaster"; import { EmployeeTempPosMaster } from "./EmployeeTempPosMaster";
import { ProfileSalaryTemp } from "./ProfileSalaryTemp"; import { ProfileSalaryTemp } from "./ProfileSalaryTemp";
import { PositionSalaryEditHistory } from "./PositionSalaryEditHistory"; import { PositionSalaryEditHistory } from "./PositionSalaryEditHistory";
import { ProfileEmployeeAbsentLate } from "./ProfileEmployeeAbsentLate";
@Entity("profileEmployee") @Entity("profileEmployee")
export class ProfileEmployee extends EntityBase { export class ProfileEmployee extends EntityBase {
@ -204,12 +203,6 @@ export class ProfileEmployee extends EntityBase {
}) })
keycloak: string; keycloak: string;
@Column({
comment: "สถานะการถูกลบผู้ใช้งานใน keycloak",
default: false,
})
isDelete: boolean;
@Column({ @Column({
comment: "ทดลองปฏิบัติหน้าที่", comment: "ทดลองปฏิบัติหน้าที่",
default: false, default: false,
@ -834,9 +827,6 @@ export class ProfileEmployee extends EntityBase {
@OneToMany(() => PositionSalaryEditHistory, (v) => v.profileEmployee) @OneToMany(() => PositionSalaryEditHistory, (v) => v.profileEmployee)
positionSalaryEditHistory: PositionSalaryEditHistory[]; positionSalaryEditHistory: PositionSalaryEditHistory[];
@OneToMany(() => ProfileEmployeeAbsentLate, (v) => v.profileEmployee)
profileEmployeeAbsentLates: ProfileEmployeeAbsentLate[];
//ที่อยู่ //ที่อยู่
@Column({ @Column({
nullable: true, nullable: true,

View file

@ -1,87 +0,0 @@
import { Entity, Column, ManyToOne, JoinColumn } from "typeorm";
import { EntityBase } from "./base/Base";
import { ProfileEmployee } from "./ProfileEmployee";
import { AbsentLateStatus, StampType } from "./ProfileAbsentLate";
@Entity("profileEmployeeAbsentLate")
export class ProfileEmployeeAbsentLate extends EntityBase {
@Column({
nullable: true,
length: 40,
comment: "คีย์นอก(FK)ของตาราง ProfileEmployee",
default: null,
})
profileEmployeeId: string;
@Column({
type: "enum",
enum: AbsentLateStatus,
comment: "สถานะ มาสาย/ขาดราชการ",
nullable: false,
})
status: AbsentLateStatus;
@Column({
type: "datetime",
comment: "วันที่และเวลาที่ลงเวลา",
nullable: false,
})
stampDate: Date;
@Column({
type: "enum",
enum: StampType,
comment: "เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย",
default: StampType.FULL_DAY,
})
stampType: StampType;
@Column({
type: "decimal",
precision: 2,
scale: 1,
comment: "จำนวน (1.0/0.5)",
default: "1.0",
})
stampAmount: number;
@Column({
type: "varchar",
length: 250,
comment: "หมายเหตุ",
nullable: true,
})
remark: string;
@Column({
comment: "สถานะลบข้อมูล",
default: false,
})
isDeleted: boolean;
@ManyToOne(() => ProfileEmployee, (profileEmployee) => profileEmployee.profileEmployeeAbsentLates)
@JoinColumn({ name: "profileEmployeeId" })
profileEmployee: ProfileEmployee;
}
// DTO Classes
export class CreateProfileEmployeeAbsentLate {
profileEmployeeId: string;
status: AbsentLateStatus;
stampDate: Date;
stampType?: StampType;
stampAmount?: number;
remark?: string;
}
export class CreateProfileEmployeeAbsentLateBatch {
records: CreateProfileEmployeeAbsentLate[];
}
export type UpdateProfileEmployeeAbsentLate = {
status?: AbsentLateStatus;
stampDate?: Date;
stampType?: StampType;
stampAmount?: number;
remark?: string;
};

View file

@ -1,17 +0,0 @@
import { Entity, Column } from "typeorm";
import { ProfileEmployeeAbsentLate } from "./ProfileEmployeeAbsentLate";
import { AbsentLateStatus, StampType } from "./ProfileAbsentLate";
@Entity("profileEmployeeAbsentLateHistory")
export class ProfileEmployeeAbsentLateHistory extends ProfileEmployeeAbsentLate {
@Column({
nullable: true,
length: 40,
comment: "คีย์นอก(FK)ของตาราง ProfileEmployeeAbsentLate",
default: null,
})
profileEmployeeAbsentLateId: string;
}
// Export enums for re-use
export { AbsentLateStatus, StampType };

View file

@ -107,24 +107,6 @@ export class ProfileLeave extends EntityBase {
}) })
isDeleted: boolean; isDeleted: boolean;
@Column({
nullable: true,
comment: "ประเภทย่อยการลา (เช่น ศึกษาต่อ, ฝึกอบรม, ปฎอบัติการวิจัย, ดูงาน)",
type: "varchar",
length: 255,
default: null,
})
leaveSubTypeName: string;
@Column({
nullable: true,
comment: "ประเทศที่ไป",
type: "varchar",
length: 255,
default: null,
})
coupleDayLevelCountry: string;
@OneToMany(() => ProfileLeaveHistory, (v) => v.profileLeave) @OneToMany(() => ProfileLeaveHistory, (v) => v.profileLeave)
histories: ProfileLeaveHistory[]; histories: ProfileLeaveHistory[];
@ -171,8 +153,6 @@ export class CreateProfileLeave {
status?: string | null; status?: string | null;
reason: string | null; reason: string | null;
leaveId?: string | null; leaveId?: string | null;
leaveSubTypeName?: string | null;
coupleDayLevelCountry?: string | null;
} }
export class CreateProfileEmployeeLeave { export class CreateProfileEmployeeLeave {
@ -186,8 +166,6 @@ export class CreateProfileEmployeeLeave {
status: string | null; status: string | null;
reason: string | null; reason: string | null;
leaveId?: string | null; leaveId?: string | null;
leaveSubTypeName?: string | null;
coupleDayLevelCountry?: string | null;
} }
export type UpdateProfileLeave = { export type UpdateProfileLeave = {
@ -199,6 +177,4 @@ export type UpdateProfileLeave = {
totalLeave?: number | null; totalLeave?: number | null;
status?: string | null; status?: string | null;
reason?: string | null; reason?: string | null;
leaveSubTypeName?: string | null;
coupleDayLevelCountry?: string | null;
}; };

View file

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

View file

@ -81,8 +81,6 @@ export class viewDirectorActing {
@ViewColumn() @ViewColumn()
posNo: string; posNo: string;
@ViewColumn() @ViewColumn()
posNoAct: string;
@ViewColumn()
posLevel: string; posLevel: string;
@ViewColumn() @ViewColumn()
posType: string; posType: string;
@ -128,6 +126,4 @@ export class viewDirectorActing {
key: string; key: string;
@ViewColumn() @ViewColumn()
positionSign: string; positionSign: string;
@ViewColumn()
positionSignChild: string;
} }

View file

@ -1,10 +0,0 @@
export interface OrgPermissionData {
root: (string | null | undefined)[] | null;
child1: (string | null | undefined)[] | null;
child2: (string | null | undefined)[] | null;
child3: (string | null | undefined)[] | null;
child4: (string | null | undefined)[] | null;
privilege?: "OWNER" | "PARENT" | "CHILD" | "BROTHER" | "NORMAL";
}
export type NodeLevel = 0 | 1 | 2 | 3 | 4 | null;

View file

@ -39,7 +39,7 @@ class CheckAuth {
} }
}); });
} }
public async PermissionOrg(req: RequestWithUser, system: string, action: string, isDirector?: boolean) { public async PermissionOrg(req: RequestWithUser, system: string, action: string) {
if ( if (
req.headers.hasOwnProperty("api_key") && req.headers.hasOwnProperty("api_key") &&
req.headers["api_key"] && req.headers["api_key"] &&
@ -56,7 +56,7 @@ class CheckAuth {
return await new CallAPI() return await new CallAPI()
.GetData(req, `/org/permission/org/${system}/${action}`) .GetData(req, `/org/permission/org/${system}/${action}`)
.then(async (x) => { .then(async (x) => {
let privilege = isDirector && isDirector === true ? "CHILD" : x.privilege; let privilege = x.privilege;
let data: any = { let data: any = {
root: [null], root: [null],
@ -288,9 +288,6 @@ class CheckAuth {
public async PermissionOrgList(req: RequestWithUser, system: string) { public async PermissionOrgList(req: RequestWithUser, system: string) {
return await this.PermissionOrg(req, system, "LIST"); 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) { public async PermissionOrgUpdate(req: RequestWithUser, system: string) {
return await this.PermissionOrg(req, system, "UPDATE"); 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) await AppDataSource.getRepository(PosMaster)
.createQueryBuilder() .createQueryBuilder()
.update(PosMaster) .update(PosMaster)
.set({ current_holderId: null, isSit: false }) .set({ current_holderId: null })
.where("id = :id", { id: findProfileInposMaster?.id }) .where("id = :id", { id: findProfileInposMaster?.id })
.execute(); .execute();
@ -293,7 +293,7 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
await AppDataSource.getRepository(PosMaster) await AppDataSource.getRepository(PosMaster)
.createQueryBuilder() .createQueryBuilder()
.update(PosMaster) .update(PosMaster)
.set({ next_holderId: null, isSit: false }) .set({ next_holderId: null })
.where("id = :id", { id: findProfileInposMasterDraft?.id }) .where("id = :id", { id: findProfileInposMasterDraft?.id })
.execute(); .execute();
@ -326,7 +326,7 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
await AppDataSource.getRepository(EmployeePosMaster) await AppDataSource.getRepository(EmployeePosMaster)
.createQueryBuilder() .createQueryBuilder()
.update(EmployeePosMaster) .update(EmployeePosMaster)
.set({ current_holderId: null, isSit: false }) .set({ current_holderId: null })
.where("id = :id", { id: findProfileInEmpPosMaster?.id }) .where("id = :id", { id: findProfileInEmpPosMaster?.id })
.execute(); .execute();
@ -395,6 +395,43 @@ export async function checkReturnCommandType(commandId: string) {
return true; 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) { export async function checkCommandType(commandId: string) {
const commandRepository = AppDataSource.getRepository(Command); const commandRepository = AppDataSource.getRepository(Command);
const commandReciveRepository = AppDataSource.getRepository(CommandRecive); const commandReciveRepository = AppDataSource.getRepository(CommandRecive);
@ -414,8 +451,6 @@ export async function checkCommandType(commandId: string) {
"C-PM-23", "C-PM-23",
"C-PM-19", "C-PM-19",
"C-PM-20", "C-PM-20",
"C-PM-25",
"C-PM-26",
"C-PM-43", "C-PM-43",
].includes(String(_type?.commandType.code)) ].includes(String(_type?.commandType.code))
) { ) {
@ -465,16 +500,6 @@ export async function checkCommandType(commandId: string) {
_retireTypeName = "ลาออกจากราชการ"; _retireTypeName = "ลาออกจากราชการ";
break; break;
} }
case "C-PM-25": {
_leaveType = "DISCIPLINE_SUSPEND";
_retireTypeName = "พักจากราชการ";
break;
}
case "C-PM-26": {
_leaveType = "DISCIPLINE_TEMP_SUSPEND";
_retireTypeName = "ให้ออกจากราชการไว้ก่อน";
break;
}
case "C-PM-43": { case "C-PM-43": {
_leaveType = "RETIRE_OUT_EMP"; _leaveType = "RETIRE_OUT_EMP";
_retireTypeName = "ให้ออกจากราชการ"; _retireTypeName = "ให้ออกจากราชการ";
@ -727,22 +752,3 @@ export function resolveNodeId(data: any) {
null 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

@ -1,149 +1,5 @@
import { DecodedJwt, createDecoder } from "fast-jwt"; import { DecodedJwt, createDecoder } from "fast-jwt";
/**
* RateLimiter
* Limits the rate of API calls to avoid overwhelming the server
*/
export class RateLimiter {
private requestsPerSecond: number;
private requestTimes: number[] = [];
constructor(requestsPerSecond: number = 10) {
this.requestsPerSecond = requestsPerSecond;
}
/**
* Throttle requests to stay within rate limit
* Waits if rate limit would be exceeded
*/
async throttle(): Promise<void> {
const now = Date.now();
// Remove timestamps older than 1 second
this.requestTimes = this.requestTimes.filter((t) => now - t < 1000);
if (this.requestTimes.length >= this.requestsPerSecond) {
const oldestRequest = this.requestTimes[0];
const waitTime = 1000 - (now - oldestRequest);
if (waitTime > 0) {
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
this.requestTimes.push(Date.now());
}
/**
* Reset the rate limiter (e.g., after a long pause)
*/
reset(): void {
this.requestTimes = [];
}
}
/**
* Check if an error is a network error (retryable)
* @param error - Error to check
* @returns true if error is network-related and retryable
*/
function isNetworkError(error: any): boolean {
if (!error) return false;
// Check for fetch network errors
if (error.name === "TypeError" && error.message.includes("fetch")) {
return true;
}
// Check for ECONNREFUSED, ETIMEDOUT, etc.
if (error.code && ["ECONNREFUSED", "ETIMEDOUT", "ECONNRESET", "ENOTFOUND"].includes(error.code)) {
return true;
}
return false;
}
/**
* Check if an HTTP status code is retryable
* @param status - HTTP status code
* @returns true if status code indicates a temporary error
*/
function isRetryableStatus(status: number): boolean {
// Retry on 5xx errors (server errors) and 429 (rate limit)
return status >= 500 || status === 429;
}
/**
* Retry wrapper with exponential backoff
* Retries failed operations with increasing delay between attempts
*
* @param fn - Function to execute
* @param maxRetries - Maximum number of retry attempts
* @param baseDelay - Base delay in milliseconds (doubles each retry)
* @returns Promise with result of fn
*/
export async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000,
): Promise<T> {
let lastError: any;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// Check if error is retryable
const isRetryable = isNetworkError(error) || isRetryableStatus(error?.status);
if (!isRetryable) {
// Don't retry on permanent errors (4xx except 429)
throw error;
}
if (attempt < maxRetries) {
// Calculate delay with exponential backoff
const delay = baseDelay * Math.pow(2, attempt);
console.log(
`[withRetry] Attempt ${attempt + 1}/${maxRetries + 1} failed, retrying in ${delay}ms...`,
error.message || error,
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
/**
* Fetch with timeout
* Aborts request if it takes longer than specified timeout
*/
async function fetchWithTimeout(
url: RequestInfo | URL,
options: RequestInit = {},
timeout: number = 10000,
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error: any) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
}
const KC_URL = process.env.KC_URL; const KC_URL = process.env.KC_URL;
const KC_REALMS = process.env.KC_REALMS; const KC_REALMS = process.env.KC_REALMS;
const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID; const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID;
@ -172,12 +28,10 @@ export function isTokenExpired(token: string, beforeExpire: number = 30) {
/** /**
* Get token from keycloak if needed * Get token from keycloak if needed
* Returns null if Keycloak is unavailable
*/ */
export async function getToken(): Promise<string | null> { export async function getToken() {
if (!KC_CLIENT_ID || !KC_SECRET) { if (!KC_CLIENT_ID || !KC_SECRET) {
console.error("[getToken] KC_CLIENT_ID and KC_SECRET are required"); throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
return null;
} }
if (token && !isTokenExpired(token)) return token; if (token && !isTokenExpired(token)) return token;
@ -188,35 +42,22 @@ export async function getToken(): Promise<string | null> {
body.append("client_secret", KC_SECRET); body.append("client_secret", KC_SECRET);
body.append("grant_type", "client_credentials"); body.append("grant_type", "client_credentials");
try { const res = await fetch(`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, {
const res = await fetchWithTimeout(
`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`,
{
method: "POST", method: "POST",
body: body, body: body,
}, }).catch((e) => console.error(e));
10000,
);
if (!res.ok) { if (!res) {
console.error(`[getToken] Keycloak token request failed: ${res.status}`); throw new Error("Cannot get token from keycloak.");
return null;
} }
const data = (await res.json()) as any; const data = (await res.json()) as any;
if (data && data.access_token) { if (data && data.access_token) {
token = data.access_token; token = data.access_token;
console.log(`[getToken] Token refreshed successfully`); }
console.log(`token: ${token}`);
return token; return token;
}
console.error("[getToken] No access_token in response");
return null;
} catch (error: any) {
console.error(`[getToken] Failed to get token: ${error.message}`);
return null;
}
} }
/** /**
@ -232,16 +73,10 @@ export async function createUser(
opts?: Record<string, any>, opts?: Record<string, any>,
token?: string, token?: string,
) { ) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[createUser] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, {
// prettier-ignore // prettier-ignore
headers: { headers: {
"authorization": `Bearer ${authToken}`, "authorization": `Bearer ${token || await getToken()}`,
"content-type": `application/json`, "content-type": `application/json`,
}, },
method: "POST", method: "POST",
@ -255,6 +90,7 @@ export async function createUser(
if (!res) return false; if (!res) return false;
if (!res.ok) { if (!res.ok) {
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
return await res.json(); return await res.json();
} }
@ -271,16 +107,10 @@ export async function createUser(
* @returns user if success, false otherwise. * @returns user if success, false otherwise.
*/ */
export async function getUser(userId: string, token?: string) { export async function getUser(userId: string, token?: string) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[getUser] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore // prettier-ignore
headers: { headers: {
"authorization": `Bearer ${authToken}`, "authorization": `Bearer ${token || await getToken()}`,
"content-type": `application/json`, "content-type": `application/json`,
}, },
}).catch((e) => console.log("Keycloak Error: ", e)); }).catch((e) => console.log("Keycloak Error: ", e));
@ -299,16 +129,10 @@ export async function getUser(userId: string, token?: string) {
* @returns user if success, false otherwise. * @returns user if success, false otherwise.
*/ */
export async function getUserByUsername(citizenId: string, token?: string) { export async function getUserByUsername(citizenId: string, token?: string) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[getUserByUsername] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, {
// prettier-ignore // prettier-ignore
headers: { headers: {
"authorization": `Bearer ${authToken}`, "authorization": `Bearer ${token || await getToken()}`,
"content-type": `application/json`, "content-type": `application/json`,
}, },
}).catch((e) => console.log("Keycloak Error: ", e)); }).catch((e) => console.log("Keycloak Error: ", e));
@ -439,38 +263,23 @@ export async function getUserCountOrg(first = "", max = "", search = "", userIds
export async function editUser(userId: string, opts: Record<string, any>) { export async function editUser(userId: string, opts: Record<string, any>) {
const { password, ...rest } = opts; const { password, ...rest } = opts;
const token = await getToken();
if (!token) {
console.error("[editUser] Failed to get Keycloak token");
return false;
}
// Get existing user data to preserve other fields
const existingUser = await getUser(userId, token);
if (!existingUser) {
console.error(`[editUser] User ${userId} not found in Keycloak`);
return false;
}
// Merge existing user data with updated fields
const updatedUser = {
...existingUser,
...rest,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
};
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore // prettier-ignore
headers: { headers: {
"authorization": `Bearer ${token}`, "authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`, "content-type": `application/json`,
}, },
method: "PUT", method: "PUT",
body: JSON.stringify(updatedUser), body: JSON.stringify({
enabled: true,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
...rest,
}),
}).catch((e) => console.log("Keycloak Error: ", e)); }).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false; if (!res) return false;
if (!res.ok) { if (!res.ok) {
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
return await res.json(); return await res.json();
} }
@ -494,24 +303,6 @@ export async function updateName(
) { ) {
// const { password, ...rest } = opts; // const { password, ...rest } = opts;
// Get existing user data to preserve other fields
const existingUser = await getUser(userId);
if (!existingUser) {
console.error(`[updateName] User ${userId} not found in Keycloak`);
return false;
}
// Merge existing user data with updated name fields
const updatedUser = {
...existingUser,
firstName,
lastName,
attributes: {
...(existingUser.attributes || {}),
prefix,
},
};
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore // prettier-ignore
headers: { headers: {
@ -519,7 +310,16 @@ export async function updateName(
"content-type": `application/json`, "content-type": `application/json`,
}, },
method: "PUT", method: "PUT",
body: JSON.stringify(updatedUser), body: JSON.stringify({
enabled: true,
// credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
// ...rest,
firstName,
lastName,
attributes: {
prefix,
},
}),
}).catch((e) => console.log("Keycloak Error: ", e)); }).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false; if (!res) return false;
@ -570,16 +370,10 @@ export async function enableStatus(userId: string, status: boolean) {
* @returns user true if success, false otherwise. * @returns user true if success, false otherwise.
*/ */
export async function deleteUser(userId: string, token?: string) { export async function deleteUser(userId: string, token?: string) {
const authToken = token || (await getToken());
if (!authToken) {
console.error("[deleteUser] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore // prettier-ignore
headers: { headers: {
"authorization": `Bearer ${authToken}`, "authorization": `Bearer ${token || await getToken()}`,
"content-type": `application/json`, "content-type": `application/json`,
}, },
method: "DELETE", method: "DELETE",
@ -961,16 +755,10 @@ export async function removeUserGroup(userId: string, groupId: string) {
// Function to change user password // Function to change user password
export async function changeUserPassword(userId: string, newPassword: string) { export async function changeUserPassword(userId: string, newPassword: string) {
try { try {
const token = await getToken();
if (!token) {
console.error("[changeUserPassword] Failed to get Keycloak token");
return false;
}
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, {
// prettier-ignore // prettier-ignore
headers: { headers: {
"authorization": `Bearer ${token}`, "authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`, "content-type": `application/json`,
}, },
method: "PUT", method: "PUT",
@ -981,15 +769,6 @@ export async function changeUserPassword(userId: string, newPassword: string) {
}), }),
}).catch((e) => console.log("Keycloak Error: ", e)); }).catch((e) => console.log("Keycloak Error: ", e));
if (!res) {
console.error("[changeUserPassword] No response from Keycloak");
return false;
}
if (!res.ok) {
console.error(`[changeUserPassword] Failed to change password: ${res.status}`);
return false;
}
return true; return true;
} catch (error) { } catch (error) {
console.error("Error changing password:", error); console.error("Error changing password:", error);
@ -1000,65 +779,60 @@ export async function changeUserPassword(userId: string, newPassword: string) {
// Function to reset password // Function to reset password
export async function resetPassword(username: string) { export async function resetPassword(username: string) {
try { try {
const token = await getToken(); // if (!API_KEY || !AUTH_ACCOUNT_SECRET) {
if (!token) { // throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
console.error("[resetPassword] Failed to get Keycloak token"); // }
return false; // const body = new URLSearchParams();
} // body.append("client_id", "gettoken");
// body.append("client_secret", AUTH_ACCOUNT_SECRET?.toString());
// body.append("grant_type", "client_credentials");
// const tokenResponse = await fetch(`${process.env.KC_URL}/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, {
// method: "POST",
// headers: {
// "Content-Type": "application/x-www-form-urlencoded",
// api_key: API_KEY,
// },
// body: body
// });
// if (!tokenResponse.ok) {
// throw new Error("Failed to get admin token");
// }
// const tokenData = await tokenResponse.json();
// const adminToken = tokenData.access_token;
const users = await fetchWithTimeout( const users = await fetch(
`${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`, `${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`,
{ {
headers: { headers: {
authorization: `Bearer ${token}`, authorization: `Bearer ${await getToken()}`,
// "authorization": `Bearer ${adminToken}`,
"content-type": `application/json`, "content-type": `application/json`,
}, },
}, },
10000,
); );
if (!users.ok) { if (!users.ok) {
const errorText = await users.text();
console.error(
`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`,
);
return false; return false;
} }
const usersData = await users.json(); const usersData = await users.json();
if (!usersData || usersData.length === 0) {
console.error(`[resetPassword] User not found with email: ${username}`);
return false;
}
const userId = usersData[0].id; const userId = usersData[0].id;
const resetResponse = await fetch(
const resetResponse = await fetchWithTimeout(
`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`, `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {
Authorization: `Bearer ${await getToken()}`, Authorization: `Bearer ${await getToken()}`,
// "Authorization": `Bearer ${adminToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(["UPDATE_PASSWORD"]), body: JSON.stringify(["UPDATE_PASSWORD"]),
}, },
10000,
); );
if (!resetResponse.ok) { if (!resetResponse.ok) {
const errorText = await resetResponse.text();
console.error(
`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`,
);
return false; return false;
} }
console.log(`[resetPassword] Password reset email sent successfully to: ${username}`);
return { message: "Password reset email sent" }; return { message: "Password reset email sent" };
} catch (error: any) { } catch (error) {
console.error(`[resetPassword] Error triggering password reset: ${error.message}`); console.error("Error triggering password reset:", error);
return false; return false;
} }
} }
@ -1068,14 +842,8 @@ export async function updateUserAttributes(
attributes: Record<string, string[]>, attributes: Record<string, string[]>,
): Promise<boolean> { ): Promise<boolean> {
try { try {
const token = await getToken();
if (!token) {
console.error("[updateUserAttributes] Failed to get Keycloak token");
return false;
}
// Get existing user data to preserve other attributes // Get existing user data to preserve other attributes
const existingUser = await getUser(userId, token); const existingUser = await getUser(userId);
if (!existingUser) { if (!existingUser) {
console.error(`User ${userId} not found in Keycloak`); console.error(`User ${userId} not found in Keycloak`);
@ -1100,7 +868,7 @@ export async function updateUserAttributes(
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
headers: { headers: {
authorization: `Bearer ${token}`, authorization: `Bearer ${await getToken()}`,
"content-type": "application/json", "content-type": "application/json",
}, },
method: "PUT", method: "PUT",
@ -1121,7 +889,7 @@ export async function updateUserAttributes(
return false; return false;
} }
// console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`); console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`);
return true; return true;
} catch (error) { } catch (error) {
console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error); console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error);

View file

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

View file

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

@ -56,7 +56,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
if (req.url.startsWith("/api/v1/org/profile/")) system = "registry"; if (req.url.startsWith("/api/v1/org/profile/")) system = "registry";
if (req.url.startsWith("/api/v1/org/profile-employee/")) system = "registry"; if (req.url.startsWith("/api/v1/org/profile-employee/")) system = "registry";
if (req.url.startsWith("/api/v1/org/profile-temp/")) system = "registry"; if (req.url.startsWith("/api/v1/org/profile-temp/")) system = "registry";
if (req.url.startsWith("/api/v1/org/ex/")) system = "retirement";
if (req.url.startsWith("/api/v1/org/commandType/admin")) system = "admin"; if (req.url.startsWith("/api/v1/org/commandType/admin")) system = "admin";
if (req.url.startsWith("/api/v1/org/commandSys/")) system = "admin"; if (req.url.startsWith("/api/v1/org/commandSys/")) system = "admin";
@ -68,28 +67,14 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4; const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4;
// // Get profile from cache // Get profile from cache
// const profileByKeycloak = await logMemoryStore.getProfileByKeycloak( const profileByKeycloak = await logMemoryStore.getProfileByKeycloak(
// req.app.locals.logData.userId, req.app.locals.logData.userId,
// ); );
// // Get rootId from cache // Get rootId from cache
// const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id); const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id);
// // console.log("ancestorDNA:", rootId); // console.log("ancestorDNA:", rootId);
// Get rootId from token
const rootId = req.app.locals.logData?.orgRootDnaId;
let _msg = data?.message;
if (!_msg) {
if (res.statusCode >= 500) {
_msg = "ไม่สำเร็จ";
} else if (res.statusCode >= 400) {
_msg = "พบข้อผิดพลาด";
} else if (res.statusCode >= 200) {
_msg = "สำเร็จ";
}
}
if (level === 1 && res.statusCode < 500) return; if (level === 1 && res.statusCode < 500) return;
if (level === 2 && res.statusCode < 400) return; if (level === 2 && res.statusCode < 400) return;
@ -106,7 +91,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
method: req.method, method: req.method,
endpoint: req.url, endpoint: req.url,
responseCode: String(res.statusCode === 304 ? 200 : res.statusCode), responseCode: String(res.statusCode === 304 ? 200 : res.statusCode),
responseDescription: _msg, responseDescription: data?.message,
input: level === 4 ? JSON.stringify(req.body, null, 2) : undefined, input: level === 4 ? JSON.stringify(req.body, null, 2) : undefined,
output: level === 4 ? JSON.stringify(data, null, 2) : undefined, output: level === 4 ? JSON.stringify(data, null, 2) : undefined,
...req.app.locals.logData, ...req.app.locals.logData,

View file

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

View file

@ -1,34 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateTableProfileAbsentLate_1774245696453 implements MigrationInterface {
name = 'CreateTableProfileAbsentLate_1774245696453'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`profileAbsentLate\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง Profile', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`profileEmployeeAbsentLate\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileEmployeeId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileEmployee', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`profileLeave\` ADD \`leaveSubTypeName\` varchar(255) NULL COMMENT 'ประเภทย่อยการลา (เช่น ศึกษาต่อ, ฝึกอบรม, ปฎอบัติการวิจัย, ดูงาน)'`);
await queryRunner.query(`ALTER TABLE \`profileLeave\` ADD \`coupleDayLevelCountry\` varchar(255) NULL COMMENT 'ประเทศที่ไป'`);
await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` ADD \`leaveSubTypeName\` varchar(255) NULL COMMENT 'ประเภทย่อยการลา (เช่น ศึกษาต่อ, ฝึกอบรม, ปฎอบัติการวิจัย, ดูงาน)'`);
await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` ADD \`coupleDayLevelCountry\` varchar(255) NULL COMMENT 'ประเทศที่ไป'`);
await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` ADD CONSTRAINT \`FK_28f5579c548da2fd76b5295d8d5\` FOREIGN KEY (\`profileId\`) REFERENCES \`profile\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` ADD CONSTRAINT \`FK_f22d4ae4155cafd14e9c6d888e6\` FOREIGN KEY (\`profileEmployeeId\`) REFERENCES \`profileEmployee\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` DROP FOREIGN KEY \`FK_f22d4ae4155cafd14e9c6d888e6\``);
await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` DROP FOREIGN KEY \`FK_28f5579c548da2fd76b5295d8d5\``);
await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` DROP COLUMN \`coupleDayLevelCountry\``);
await queryRunner.query(`ALTER TABLE \`profileLeaveHistory\` DROP COLUMN \`leaveSubTypeName\``);
await queryRunner.query(`ALTER TABLE \`profileLeave\` DROP COLUMN \`coupleDayLevelCountry\``);
await queryRunner.query(`ALTER TABLE \`profileLeave\` DROP COLUMN \`leaveSubTypeName\``);
await queryRunner.query(`DROP TABLE \`profileEmployeeAbsentLate\``);
await queryRunner.query(`DROP TABLE \`profileAbsentLate\``);
}
}

View file

@ -1,21 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdateTableProfileAbsentLate1774253766170 implements MigrationInterface {
name = 'UpdateTableProfileAbsentLate1774253766170'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` DROP COLUMN \`stampDate\``);
await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` ADD \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา'`);
await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` DROP COLUMN \`stampDate\``);
await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` ADD \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` DROP COLUMN \`stampDate\``);
await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLate\` ADD \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา'`);
await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` DROP COLUMN \`stampDate\``);
await queryRunner.query(`ALTER TABLE \`profileAbsentLate\` ADD \`stampDate\` date NOT NULL COMMENT 'วันที่ลงเวลา'`);
}
}

View file

@ -1,19 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddTableAbsentLateHistory1774408245407 implements MigrationInterface {
name = 'AddTableAbsentLateHistory1774408245407'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`profileEmployeeAbsentLateHistory\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileEmployeeId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileEmployee', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, \`profileEmployeeAbsentLateId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileEmployeeAbsentLate', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`profileAbsentLateHistory\` (\`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL COMMENT 'สร้างข้อมูลเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`createdUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่สร้างข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`lastUpdatedAt\` datetime(6) NOT NULL COMMENT 'แก้ไขข้อมูลล่าสุดเมื่อ' DEFAULT CURRENT_TIMESTAMP(6), \`lastUpdateUserId\` varchar(40) NOT NULL COMMENT 'User Id ที่แก้ไขข้อมูล' DEFAULT '00000000-0000-0000-0000-000000000000', \`createdFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่สร้างข้อมูล' DEFAULT 'System Administrator', \`lastUpdateFullName\` varchar(200) NOT NULL COMMENT 'ชื่อ User ที่แก้ไขข้อมูลล่าสุด' DEFAULT 'System Administrator', \`profileId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง Profile', \`status\` enum ('LATE', 'ABSENT') NOT NULL COMMENT 'สถานะ มาสาย/ขาดราชการ', \`stampDate\` datetime NOT NULL COMMENT 'วันที่และเวลาที่ลงเวลา', \`stampType\` enum ('FULL_DAY', 'MORNING', 'AFTERNOON') NOT NULL COMMENT 'เต็มวัน/ครึ่งเช้า/ครึ่งบ่าย' DEFAULT 'FULL_DAY', \`stampAmount\` decimal(2,1) NOT NULL COMMENT 'จำนวน (1.0/0.5)' DEFAULT '1.0', \`remark\` varchar(250) NULL COMMENT 'หมายเหตุ', \`isDeleted\` tinyint NOT NULL COMMENT 'สถานะลบข้อมูล' DEFAULT 0, \`profileAbsentLateId\` varchar(40) NULL COMMENT 'คีย์นอก(FK)ของตาราง ProfileAbsentLate', PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`profileEmployeeAbsentLateHistory\` ADD CONSTRAINT \`FK_8b06ca79d6f75c7d6577c86f3d4\` FOREIGN KEY (\`profileEmployeeId\`) REFERENCES \`profileEmployee\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE \`profileAbsentLateHistory\` ADD CONSTRAINT \`FK_0fa6a843d0e6d901a4f2f56c541\` FOREIGN KEY (\`profileId\`) REFERENCES \`profile\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE \`profileAbsentLateHistory\``);
await queryRunner.query(`DROP TABLE \`profileEmployeeAbsentLateHistory\``);
}
}

View file

@ -1,23 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdateProfileAndProfileemployeeAddFieldIsdelete1775112029663 implements MigrationInterface {
name = 'UpdateProfileAndProfileemployeeAddFieldIsdelete1775112029663'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`profileEmployee\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`);
await queryRunner.query(`ALTER TABLE \`profileEmployeeHistory\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`);
await queryRunner.query(`ALTER TABLE \`profile\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`);
await queryRunner.query(`ALTER TABLE \`profileHistory\` ADD \`isDelete\` tinyint NOT NULL COMMENT 'สถานะการถูกลบผู้ใช้งานใน keycloak' DEFAULT 0`);
// Update ข้อมูลเดิม: ถ้า keycloak null → isDelete = true (1), ถ้ามีค่า → isDelete = false (0)
await queryRunner.query(`UPDATE \`profileEmployee\` SET \`isDelete\` = CASE WHEN \`keycloak\` IS NULL THEN 1 ELSE 0 END`);
await queryRunner.query(`UPDATE \`profile\` SET \`isDelete\` = CASE WHEN \`keycloak\` IS NULL THEN 1 ELSE 0 END`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`profileHistory\` DROP COLUMN \`isDelete\``);
await queryRunner.query(`ALTER TABLE \`profile\` DROP COLUMN \`isDelete\``);
await queryRunner.query(`ALTER TABLE \`profileEmployeeHistory\` DROP COLUMN \`isDelete\``);
await queryRunner.query(`ALTER TABLE \`profileEmployee\` DROP COLUMN \`isDelete\``);
}
}

View file

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

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddStatusEnumToIssues1778208324657 implements MigrationInterface {
name = 'AddStatusEnumToIssues1778208324657'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`issues\` CHANGE \`status\` \`status\` enum ('NEW', 'IN_PROGRESS', 'RESOLVED', 'CLOSED', 'HELPDESK_IN_PROGRESS', 'REPLIED') NOT NULL COMMENT 'สถานะการแก้ไขปัญหา' DEFAULT 'NEW'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`issues\` CHANGE \`status\` \`status\` enum ('NEW', 'IN_PROGRESS', 'RESOLVED', 'CLOSED') NOT NULL COMMENT 'สถานะการแก้ไขปัญหา' DEFAULT 'NEW'`);
}
}

View file

@ -1,23 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,27 +0,0 @@
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

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

View file

@ -1,232 +0,0 @@
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

@ -1,146 +0,0 @@
import { AppDataSource } from "../database/data-source";
import { CommandRecive } from "../entities/CommandRecive";
import { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot";
import { Profile } from "../entities/Profile";
export interface PosNumCodeSitResult {
posNumCodeSit: string;
posNumCodeSitAbb: string;
commandTypeName: string;
commandNo: string;
commandYear: number;
commandExcecuteDate: Date;
}
/**
*
* @param reciveId commandRecive.Id
* @param code
* @returns Promise<void>
*/
export async function reOrderCommandRecivesAndDelete(
reciveId: string
): Promise<void> {
const commandReciveRepo = AppDataSource.getRepository(CommandRecive);
const commandRepo = AppDataSource.getRepository(Command);
// ค้นหาข้อมูลผู้ได้รับคำสั่งตาม reciveId
const commandRecive = await commandReciveRepo.findOne({
where: { id: reciveId }
});
if (commandRecive == null)
return;
const commandId = commandRecive.commandId;
// ลบตาม refId
await commandReciveRepo.delete(commandRecive.id);
const commandReciveList = await commandReciveRepo.find({
where: { commandId: commandId },
order: { order: "ASC" },
});
// ลำดับผู้ได้รับคำสั่งใหม่
if (commandReciveList.length > 0) {
await Promise.all(
commandReciveList.map(async (p, i) => {
p.order = i + 1;
await commandReciveRepo.save(p);
})
);
} else {
// ถ้าไม่มีผู้ได้รับคำสั่งเหลือเลย ให้ยกเลิกคำสั่ง
await commandRepo.update({ id: commandId }, { status: "CANCEL" });
}
}
/**
* posNumCodeSit posNumCodeSitAbb commandId
* @param commandId ID
* @param status ()
* @returns Promise<PosNumCodeSitResult>
*/
export async function getPosNumCodeSit(
commandId: string,
status?: string
): Promise<PosNumCodeSitResult> {
const commandRepo = AppDataSource.getRepository(Command);
const orgRootRepo = AppDataSource.getRepository(OrgRoot);
const profileRepo = AppDataSource.getRepository(Profile);
let posNumCodeSit:string = "";
let posNumCodeSitAbb:string = "";
let commandTypeName:string = "";
let commandNo:string = "";
let commandYear:number = 0;
let commandExcecuteDate:Date = new Date;
let _command: Command | null;
if (!status) {
_command = await commandRepo.findOne({
where: { id: commandId },
relations: { commandType: true }
});
}
else {
_command = await commandRepo.findOne({
where: { id: commandId, status: status },
relations: { commandType: true }
});
}
if (_command) {
if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") {
const orgRootDeputy = await orgRootRepo.findOne({
where: {
isDeputy: true,
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
},
relations: ["orgRevision"],
});
posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร";
posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป.";
} else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") {
posNumCodeSit = "กรุงเทพมหานคร";
posNumCodeSitAbb = "กทม.";
} else {
let _profileAdmin = await profileRepo.findOne({
where: {
keycloak: _command?.createdUserId.toString(),
current_holders: {
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
},
},
relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"],
});
posNumCodeSit =
_profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ??
"";
posNumCodeSitAbb =
_profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot
.orgRootShortName ?? "";
}
commandTypeName = _command?.commandType?.name ?? "";
commandNo = _command?.commandNo ?? "";
commandYear = _command?.commandYear;
commandExcecuteDate = _command?.commandExcecuteDate;
}
return {
posNumCodeSit,
posNumCodeSitAbb,
commandTypeName,
commandNo,
commandYear,
commandExcecuteDate,
};
}

Some files were not shown because too many files have changed in this diff Show more