diff --git a/docs/batch-update-optimization.md b/docs/batch-update-optimization.md deleted file mode 100644 index 8496d50e..00000000 --- a/docs/batch-update-optimization.md +++ /dev/null @@ -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 { - 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 { - 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* diff --git a/docs/hrms-api-org-error-report.md b/docs/hrms-api-org-error-report.md deleted file mode 100644 index 630108ab..00000000 --- a/docs/hrms-api-org-error-report.md +++ /dev/null @@ -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* diff --git a/docs/migrations/fix_GetProfileEmployeeSalaryLevel_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileEmployeeSalaryLevel_calendar_arithmetic.sql deleted file mode 100644 index bca30538..00000000 --- a/docs/migrations/fix_GetProfileEmployeeSalaryLevel_calendar_arithmetic.sql +++ /dev/null @@ -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 ; diff --git a/docs/migrations/fix_GetProfileEmployeeSalaryPosition_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileEmployeeSalaryPosition_calendar_arithmetic.sql deleted file mode 100644 index fa53b467..00000000 --- a/docs/migrations/fix_GetProfileEmployeeSalaryPosition_calendar_arithmetic.sql +++ /dev/null @@ -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 ; diff --git a/docs/migrations/fix_GetProfileSalaryExecutive_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileSalaryExecutive_calendar_arithmetic.sql deleted file mode 100644 index 9b1d5d50..00000000 --- a/docs/migrations/fix_GetProfileSalaryExecutive_calendar_arithmetic.sql +++ /dev/null @@ -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 ; diff --git a/docs/migrations/fix_GetProfileSalaryLevel_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileSalaryLevel_calendar_arithmetic.sql deleted file mode 100644 index 0ce8bbb5..00000000 --- a/docs/migrations/fix_GetProfileSalaryLevel_calendar_arithmetic.sql +++ /dev/null @@ -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 ; diff --git a/docs/migrations/fix_GetProfileSalaryPosition_calendar_arithmetic.sql b/docs/migrations/fix_GetProfileSalaryPosition_calendar_arithmetic.sql deleted file mode 100644 index aed2e9e7..00000000 --- a/docs/migrations/fix_GetProfileSalaryPosition_calendar_arithmetic.sql +++ /dev/null @@ -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'); diff --git a/reports/SUMMARY-CONTROLLERS-ANALYSIS.md b/reports/SUMMARY-CONTROLLERS-ANALYSIS.md deleted file mode 100644 index 42bdad8e..00000000 --- a/reports/SUMMARY-CONTROLLERS-ANALYSIS.md +++ /dev/null @@ -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( - items: T[], - executor: (item: T, index: number) => Promise, - 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( - operation: (entityManager: EntityManager) => Promise -): Promise { - 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** diff --git a/reports/batch-01-controllers-1-10-analysis.md b/reports/batch-01-controllers-1-10-analysis.md deleted file mode 100644 index c594def5..00000000 --- a/reports/batch-01-controllers-1-10-analysis.md +++ /dev/null @@ -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** diff --git a/reports/batch-02-controllers-11-20-analysis.md b/reports/batch-02-controllers-11-20-analysis.md deleted file mode 100644 index 19cb57d5..00000000 --- a/reports/batch-02-controllers-11-20-analysis.md +++ /dev/null @@ -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** diff --git a/reports/batch-03-controllers-21-30-analysis.md b/reports/batch-03-controllers-21-30-analysis.md deleted file mode 100644 index 32fa1397..00000000 --- a/reports/batch-03-controllers-21-30-analysis.md +++ /dev/null @@ -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 { - 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 { - 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, - @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, - @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( - items: T[], - executor: (item: T, index: number) => Promise, - 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** diff --git a/reports/batch-04-controllers-31-40-analysis.md b/reports/batch-04-controllers-31-40-analysis.md deleted file mode 100644 index df272b20..00000000 --- a/reports/batch-04-controllers-31-40-analysis.md +++ /dev/null @@ -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** \ No newline at end of file diff --git a/reports/batch-05-controllers-41-50-analysis.md b/reports/batch-05-controllers-41-50-analysis.md deleted file mode 100644 index cf3cb790..00000000 --- a/reports/batch-05-controllers-41-50-analysis.md +++ /dev/null @@ -1,1060 +0,0 @@ -# รายงานการวิเคราะห์ความเสี่ยงการหยุดทำงานของระบบ (Crash Risk Analysis) -## ชุดที่ 5 (Batch 5) - วันที่ 8 พฤษภาคม 2568 - ---- - -## **สรุปผลการตรวจสอบ** - -### จำนวนไฟล์ที่ตรวจสอบ: 10 Controllers -### จำนวนปัญหาที่พบ: 12 ปัญหา - -### ระดับความรุนแรง: -- 🔴 **วิกฤติ (4 รายการ)**: มีโอกาสทำให้ Service Crash สูงมาก -- 🟠 **สูง (5 รายการ)**: มีโอกาสทำให้เกิด Unhandled Exception -- 🟡 **ปานกลาง (3 รายการ)**: อาจทำให้เกิดปัญหาในสถานการณ์เฉพาะ - ---- - -## **รายละเอียดปัญหาแต่ละรายการ** - ---- - -## 🔴 **ปัญหาที่ 1: Redis Client Connection Leak** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PermissionController.ts` -- **บรรทัด:** 40-44, 132-136, 472-476, 581-585, 669-673, 775-779, 947-951 -- **Method:** `getPermission`, `listAuthSys`, `listAuthSysOrg`, `listOrgUser`, `getPermissionFunc`, `listAuthSysOrgFunc`, `checkOrg` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Resource Leak และ Connection ไม่ถูกปิด - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -โค้ดสร้าง Redis Client ใหม่ทุกครั้งที่มีการเรียกใช้ method แต่: -1. **ไม่มีการปิด connection**: Redis client ไม่ถูก close หลังใช้งาน -2. **Connection pool exhaustion**: หากมี request จำนวนมาก จะทำให้หมด connection -3. **Memory leak**: Redis client objects สะสมใน memory -4. **Service crash**: เมื่อถึง limit ของ Redis connection หรือ memory จะทำให้ service หยุดทำงาน - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -@Get("") -public async getPermission(@Request() request: RequestWithUser) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); - - // ... ใช้งาน redisClient - - redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); - // ❌ ไม่มีการปิด connection - return new HttpSuccess(reply); -} -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -@Get("") -public async getPermission(@Request() request: RequestWithUser) { - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); - - let profile: any = await this.profileRepo.findOne({ - select: ["id"], - where: { keycloak: request.user.sub }, - }); - - let reply = await getAsync("role_" + profile.id); - if (reply != null) { - reply = JSON.parse(reply); - } else { - // ... logic เดิม - redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); - } - - return new HttpSuccess(reply); - } finally { - // ✅ ปิด connection เสมอ - if (redisClient) { - redisClient.quit(); - // หรือใช้ redisClient.end(true) สำหรับ force close - } - } -} -``` - -### วิธีแก้ไขที่ดีกว่า (ใช้ Connection Pool): -```typescript -// สร้าง singleton Redis client -export class RedisService { - private static client: any = null; - - static async getClient() { - if (!this.client) { - this.client = require("redis").createClient({ - host: process.env.REDIS_HOST, - port: parseInt(process.env.REDIS_PORT || "6379"), - retry_strategy: (options) => { - if (options.total_retry_time > 1000 * 60 * 60) { - return new Error("Retry time exhausted"); - } - return Math.min(options.attempt * 100, 3000); - }, - }); - - this.client.on("error", (err: Error) => { - console.error("Redis Client Error:", err); - }); - } - return this.client; - } -} - -// ใช้งานใน controller -@Get("") -public async getPermission(@Request() request: RequestWithUser) { - const redisClient = await RedisService.getClient(); - const getAsync = promisify(redisClient.get).bind(redisClient); - - let profile: any = await this.profileRepo.findOne({ - select: ["id"], - where: { keycloak: request.user.sub }, - }); - - let reply = await getAsync("role_" + profile.id); - if (reply != null) { - reply = JSON.parse(reply); - } else { - // ... logic เดิม - redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); - } - - return new HttpSuccess(reply); -} -``` - ---- - -## 🔴 **ปัญหาที่ 2: Unhandled Promise Rejection ใน forEach พร้อม Async** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PosMasterActController.ts` -- **บรรทัด:** 317-320 -- **Method:** `deletePosMasterAct` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Unhandled Promise Rejection - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -การใช้ `forEach` กับ async function โดยไม่มีการรอ: -1. **Promise ไม่ถูก await**: การ save หลายรายการเกิดขึ้น parallel โดยไม่มีการรอ -2. **Unhandled rejection**: หาก save fail จะเกิด unhandled rejection -3. **Race condition**: ข้อมูลอาจไม่ถูกต้องหากมีการอัปเดตพร้อมกัน -4. **Process crash**: ใน Node.js บาง version จะ crash เมื่อมี unhandled rejection - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -if (posMasterAct != null) { - const posMasterActList = await this.posMasterActRepository.find({ - where: { - posMasterId: posMasterAct.posMasterId, - }, - }); - posMasterActList.forEach(async (p, i) => { - p.posMasterOrder = i + 1; - await this.posMasterActRepository.save(p); - }); - // ❌ forEach ไม่รอ async ให้เสร็จ -} -return new HttpSuccess(); -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -if (posMasterAct != null) { - const posMasterActList = await this.posMasterActRepository.find({ - where: { - posMasterId: posMasterAct.posMasterId, - }, - }); - - // ✅ วิธีที่ 1: ใช้ for...of loop (sequential) - for (const [i, p] of posMasterActList.entries()) { - p.posMasterOrder = i + 1; - await this.posMasterActRepository.save(p); - } - - // หรือ ✅ วิธีที่ 2: ใช้ Promise.all (parallel) - await Promise.all( - posMasterActList.map(async (p, i) => { - p.posMasterOrder = i + 1; - await this.posMasterActRepository.save(p); - }) - ); -} -return new HttpSuccess(); -``` - ---- - -## 🔴 **ปัญหาที่ 3: Promise.all ที่ซ้อนกันโดยไม่มี Error Handling** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PosMasterActController.ts` -- **บรรทัด:** 771-835 -- **Method:** `activePosMasterAct` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Unhandled Promise Rejection ใน Nested Promise.all - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -โค้ดใช้ `Promise.all` ซ้อนกัน 2 ชั้นโดยไม่มี try-catch: -1. **Error propagate ไม่ถูกต้อง**: หาก promise ใด fail จะไม่ถูกจัดการ -2. **Partial failure**: บางส่วนอาจสำเร็จ บางส่วน fail โดยไม่มีการ rollback -3. **Unhandled rejection**: จะทำให้ process crash ใน production - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -await Promise.all( - posMasterActs.map(async (posMasterAct) => { - // ... logic ยาวๆ - - if (existingActivePositions.length > 0) { - await Promise.all( - existingActivePositions.map(async (pos) => { - // ❌ ไม่มี error handling รอบๆ - Object.assign(pos, { - status: false, - lastUpdateUserId: req.user?.sub ?? null, - lastUpdateFullName: req.user?.name ?? null, - lastUpdatedAt: new Date(), - dateEnd: new Date(), - }); - await this.actpositionRepository.save(pos); - }), - ); - } - - const dataAct = new ProfileActposition(); - // ... สร้าง dataAct - await this.actpositionRepository.save(dataAct); - - posMasterAct.statusReport = "DONE"; - await this.posMasterActRepository.save(posMasterAct); - }), -); -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -try { - await Promise.all( - posMasterActs.map(async (posMasterAct) => { - try { - const orgShortName = [ - posMasterAct.posMaster?.orgChild4?.orgChild4ShortName, - posMasterAct.posMaster?.orgChild3?.orgChild3ShortName, - posMasterAct.posMaster?.orgChild2?.orgChild2ShortName, - posMasterAct.posMaster?.orgChild1?.orgChild1ShortName, - posMasterAct.posMaster?.orgRoot?.orgRootShortName, - ].find(Boolean) ?? ""; - - const profileId = posMasterAct.posMasterChild?.current_holderId; - - if (profileId) { - const existingActivePositions = await this.actpositionRepository.find({ - select: [ - "id", - "status", - "lastUpdateUserId", - "lastUpdateFullName", - "lastUpdatedAt", - "dateEnd", - "isDeleted" - ], - where: { profileId, status: true, isDeleted: false }, - }); - - if (existingActivePositions.length > 0) { - // ✅ เพิ่ม error handling ใน inner Promise.all - await Promise.all( - existingActivePositions.map(async (pos) => { - try { - Object.assign(pos, { - status: false, - lastUpdateUserId: req.user?.sub ?? null, - lastUpdateFullName: req.user?.name ?? null, - lastUpdatedAt: new Date(), - dateEnd: new Date(), - }); - await this.actpositionRepository.save(pos); - } catch (error) { - // ✅ Log error แต่ไม่ให้ทั้ง batch fail - console.error(`ไม่สามารถอัปเดตตำแหน่ง ${pos.id}:`, error); - } - }) - ); - } - } - - const dataAct = new ProfileActposition(); - Object.assign(dataAct, { - profileId: profileId ?? null, - dateStart: new Date(), - posNo: - orgShortName && posMasterAct.posMaster?.posMasterNo - ? `${orgShortName} ${posMasterAct.posMaster.posMasterNo}` - : posMasterAct.posMaster?.posMasterNo ?? "-", - position: posMasterAct.posMaster?.current_holder?.position ?? null, - posNoAbb: orgShortName, - status: true, - createdUserId: req.user?.sub ?? null, - createdFullName: req.user?.name ?? null, - lastUpdateUserId: req.user?.sub ?? null, - lastUpdateFullName: req.user?.name ?? null, - }); - - await this.actpositionRepository.save(dataAct); - - posMasterAct.statusReport = "DONE"; - await this.posMasterActRepository.save(posMasterAct); - } catch (error) { - // ✅ Log error แต่ทำต่อรายการอื่น - console.error(`ไม่สามารถ activate ตำแหน่ง ${posMasterAct.id}:`, error); - // อาจต้องการ mark เป็น FAILED - posMasterAct.statusReport = "FAILED"; - await this.posMasterActRepository.save(posMasterAct).catch(e => { - console.error("ไม่สามารถบันทึก status:", e); - }); - } - }), - ); -} catch (error) { - console.error("เกิดข้อผิดพลาดในการ activate ตำแหน่ง:", error); - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - "ไม่สามารถดำเนินการได้ กรุณาลองใหม่" - ); -} - -return new HttpSuccess(); -``` - ---- - -## 🔴 **ปัญหาที่ 4: String Throw ที่ไม่ใช่ Error Object** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PermissionController.ts` -- **บรรทัด:** 763, 770 -- **Method:** `Permission` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Throwing String แทน Error Object - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -โค้ด throw string แทนที่จะ throw Error object: -1. **Stack trace หาย**: ไม่สามารถ trace ตำแหน่งที่เกิด error ได้ -2. **Error handler ไม่ทำงาน**: Global error handlers อาจไม่รู้จัก string errors -3. **Monitoring ไม่เจอ**: Error tracking systems อาจไม่สามารถจับ error ได้ -4. **Debug ยาก**: ไม่มี stack trace ทำให้หาต้นตอได้ยาก - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -public async Permission(req: RequestWithUser, system: string, action: string) { - let x: any = await this.getPermissionFunc(req); - let permission = false; - let role = x.roles.find((x: any) => x.authSysId == system); - if (!role) throw "ไม่มีสิทธิ์เข้าระบบ"; // ❌ throw string - if (role.attrOwnership == "OWNER") return "OWNER"; - if (action.trim().toLocaleUpperCase() == "CREATE") permission = role.attrIsCreate; - if (action.trim().toLocaleUpperCase() == "DELETE") permission = role.attrIsDelete; - if (action.trim().toLocaleUpperCase() == "GET") permission = role.attrIsGet; - if (action.trim().toLocaleUpperCase() == "LIST") permission = role.attrIsList; - if (action.trim().toLocaleUpperCase() == "UPDATE") permission = role.attrIsUpdate; - if (permission == false) throw "ไม่มีสิทธิ์ใช้งานระบบนี้"; // ❌ throw string - return role.attrPrivilege; -} -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -public async Permission(req: RequestWithUser, system: string, action: string) { - let x: any = await this.getPermissionFunc(req); - let permission = false; - let role = x.roles.find((x: any) => x.authSysId == system); - - if (!role) { - // ✅ throw HttpError แทน string - throw new HttpError( - HttpStatus.FORBIDDEN, - "ไม่มีสิทธิ์เข้าระบบ" - ); - } - - if (role.attrOwnership == "OWNER") return "OWNER"; - - const normalizedAction = action.trim().toLocaleUpperCase(); - if (normalizedAction == "CREATE") permission = role.attrIsCreate; - else if (normalizedAction == "DELETE") permission = role.attrIsDelete; - else if (normalizedAction == "GET") permission = role.attrIsGet; - else if (normalizedAction == "LIST") permission = role.attrIsList; - else if (normalizedAction == "UPDATE") permission = role.attrIsUpdate; - - if (permission == false) { - // ✅ throw HttpError แทน string - throw new HttpError( - HttpStatus.FORBIDDEN, - "ไม่มีสิทธิ์ใช้งานระบบนี้" - ); - } - - return role.attrPrivilege; -} -``` - ---- - -## 🟠 **ปัญหาที่ 5: Null Reference บน Array.find()** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PermissionProfileController.ts` -- **บรรทัด:** 68-69 -- **Method:** `GetActiveRootIdAdmin` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Null Reference Error - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -การเข้าถึง property ของผลลัพธ์จาก `find()` โดยไม่ตรวจสอบ: -1. **Optional chaining ไม่ครบ**: `.find()` อาจ return undefined -2. **Access property ของ undefined**: จะทำให้เกิด error - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -rootId = - orgRevisionActive?.posMasters?.filter((x) => x.next_holderId == profile.id)[0] - ?.orgRootId || null; -// ❌ [0] อาจเป็น undefined หาก filter result ว่างเปล่า -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -const posMaster = orgRevisionActive?.posMasters?.find((x) => x.next_holderId == profile.id); -rootId = posMaster?.orgRootId || null; -// ✅ ใช้ .find() แทน .filter()[0] เพื่อความชัดเจน -``` - ---- - -## 🟠 **ปัญหาที่ 6: SQL Injection ใน Dynamic Query** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PermissionOrgController.ts` -- **บรรทัด:** 76-78 -- **Method:** `GetActiveRootIdAdmin` - -### ประเภทปัญหา: -1. **Unhandled Exception** - SQL Injection Risk - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -การใส่ค่าโดยตรงลงใน query string: -1. **SQL injection**: ผู้ไม่ประสงค์ดีอาจ inject SQL code -2. **Query syntax error**: หากมีอักขระพิเศษอาจทำให้ query fail -3. **Database crash**: Query ที่ผิดพลาดอาจทำให้ database หยุดทำงาน - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -const data = await AppDataSource.getRepository(OrgRoot) - .createQueryBuilder("orgRoot") - .where("orgRoot.orgRevisionId = :id", { id: orgRevisionActive.id }) - .andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", { - rootId: rootId, - }) - .orderBy("orgRoot.orgRootOrder", "ASC") - .getMany(); -// ❌ ใส่ condition โดยตรงเป็น string -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -const queryBuilder = AppDataSource.getRepository(OrgRoot) - .createQueryBuilder("orgRoot") - .where("orgRoot.orgRevisionId = :id", { id: orgRevisionActive.id }); - -if (rootId != null) { - queryBuilder.andWhere("orgRoot.id = :rootId", { rootId }); -} - -const data = await queryBuilder - .orderBy("orgRoot.orgRootOrder", "ASC") - .getMany(); -// ✅ ใช้ query builder ที่ปลอดภัย -``` - ---- - -## 🟠 **ปัญหาที่ 7: Race Condition ใน Promise.all.map()** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PosMasterActController.ts` -- **บรรทัด:** 413-443 -- **Method:** `GetPosMasterActProfile` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Race Condition ใน Async Mapping - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -การใช้ `Promise.all()` ร่วมกับ `.map().sort()`: -1. **Sort ผิดพลาด**: การ sort หลังจาก Promise.all อาจไม่ทำงานตามที่คาดหวัง -2. **Unhandled promise rejection**: หาก promise ใด fail จะเกิด unhandled rejection -3. **Memory spike**: โหลดข้อมูลทั้งหมดพร้อมกันอาจทำให้ memory เต็ม - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -const data = await Promise.all( - posMasterActs - .sort((a, b) => a.posMaster.posMasterOrder - b.posMaster.posMasterOrder) - .map((item) => { - // ... process item - return { - id: item.id, - // ... ส่งคืนข้อมูล - }; - }), -); -// ❌ Promise.all ไม่รับประกันลำดับ -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -// ✅ วิธีที่ 1: Sort หลังจาก Promise.all -const processedData = await Promise.all( - posMasterActs.map(async (item) => { - const shortName = - item.posMasterChild != null && item.posMasterChild.orgChild4 != null - ? `${item.posMasterChild.orgChild4.orgChild4ShortName} ${item.posMasterChild.posMasterNo}` - : item.posMasterChild != null && item.posMasterChild?.orgChild3 != null - ? `${item.posMasterChild.orgChild3.orgChild3ShortName} ${item.posMasterChild.posMasterNo}` - : item.posMasterChild != null && item.posMasterChild?.orgChild2 != null - ? `${item.posMasterChild.orgChild2.orgChild2ShortName} ${item.posMasterChild.posMasterNo}` - : item.posMasterChild != null && item.posMasterChild?.orgChild1 != null - ? `${item.posMasterChild.orgChild1.orgChild1ShortName} ${item.posMasterChild.posMasterNo}` - : item.posMasterChild != null && item.posMasterChild?.orgRoot != null - ? `${item.posMasterChild.orgRoot.orgRootShortName} ${item.posMasterChild.posMasterNo}` - : null; - - return { - id: item.id, - posMasterOrder: item.posMasterOrder, - profileId: item.posMasterChild?.current_holder?.id ?? null, - citizenId: item.posMasterChild?.current_holder?.citizenId ?? null, - prefix: item.posMasterChild?.current_holder?.prefix ?? null, - firstName: item.posMasterChild?.current_holder?.firstName ?? null, - lastName: item.posMasterChild?.current_holder?.lastName ?? null, - posLevel: item.posMasterChild?.current_holder?.posLevel?.posLevelName ?? null, - posType: item.posMasterChild?.current_holder?.posType?.posTypeName ?? null, - position: item.posMasterChild?.current_holder?.position ?? null, - posNo: shortName, - }; - }) -); - -// ✅ Sort หลังจาก process เสร็จ -const data = processedData.sort((a, b) => a.posMasterOrder - b.posMasterOrder); - -return new HttpSuccess(data); -``` - ---- - -## 🟠 **ปัญหาที่ 8: Promise.all ที่ไม่มี Error Handling** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PermissionProfileController.ts` -- **บรรทัด:** 162-249 -- **Method:** `listProfile` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Promise.all โดยไม่มี Error Handling - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -การใช้ Promise.all กับ array mapping ที่ซับซ้อน: -1. **Unhandled rejection**: หากการ process รายการใด fail ทั้งหมดจะ fail -2. **Complex null checks**: Logic ซับซ้อนทำให้เกิด error ได้ง่าย -3. **Nested optional chaining**: หาก data ไม่สมบูรณ์อาจ throw error - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -const data = await Promise.all( - record.map((_data) => { - const shortName = - _data.current_holders.length == 0 - ? null - : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null - ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` - : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null - ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` - : null; // ... ยาวมาก - - return { - id: _data.id, - // ... ส่งคืนข้อมูล - }; - }), -); -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -const data = await Promise.all( - record.map((_data) => { - try { - // ✅ แยก logic ออกเป็น function หรือ helper - const currentHolder = _data.current_holders?.find( - (x) => x.orgRevisionId == findRevision.id - ); - - const shortName = this.getShortName(currentHolder); - const root = currentHolder?.orgRoot; - const child1 = currentHolder?.orgChild1; - const child2 = currentHolder?.orgChild2; - const child3 = currentHolder?.orgChild3; - const child4 = currentHolder?.orgChild4; - - return { - id: _data.id, - avatar: _data.avatar, - avatarName: _data.avatarName, - prefix: _data.prefix, - rank: _data.rank, - firstName: _data.firstName, - lastName: _data.lastName, - org: this.formatOrgName(child4, child3, child2, child1, root), - posNo: shortName, - position: _data.position, - posType: _data.posType?.posTypeName ?? null, - posLevel: _data.posLevel?.posLevelName ?? null, - }; - } catch (error) { - console.error(`Error processing profile ${_data.id}:`, error); - // ✅ Return default value หรือ skip - return { - id: _data.id, - avatar: _data.avatar, - avatarName: _data.avatarName, - prefix: _data.prefix, - rank: _data.rank, - firstName: _data.firstName, - lastName: _data.lastName, - org: null, - posNo: null, - position: _data.position, - posType: null, - posLevel: null, - }; - } - }), -); -``` - ---- - -## 🟠 **ปัญหาที่ 9: String Throw ใน PosTypeController** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PosTypeController.ts` -- **บรรทัด:** 52-54 -- **Method:** `createType` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Logic Error: ตรวจสอบ null หลังจาก Object.assign - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -โค้ดตรวจสอบ null หลังจาก `Object.assign`: -1. **Check ไม่เคยเป็น true**: Object.assign จะสร้าง object เสมอ -2. **Dead code**: บรรทัด throw error จะไม่ทำงานเลย - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -async createType( - @Body() - requestBody: CreatePosType, - @Request() request: RequestWithUser, -) { - const posType = Object.assign(new PosType(), requestBody); - if (!posType) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล"); - } - // ❌ Object.assign เสมอ return object ไม่เคยเป็น null -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -async createType( - @Body() - requestBody: CreatePosType, - @Request() request: RequestWithUser, -) { - if (!requestBody || !requestBody.posTypeName) { - throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณาระบุชื่อประเภทตำแหน่ง"); - } - - const posType = Object.assign(new PosType(), requestBody); - // ✅ ตรวจสอบ input ก่อนสร้าง object -``` - ---- - -## 🟠 **ปัญหาที่ 10: Promise.all ที่ไม่มี Error Handling ใน PermissionOrgController** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PermissionOrgController.ts` -- **บรรทัด:** 162-249 -- **Method:** `listProfile` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Promise.all ใน Complex Mapping Logic - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -เหมือนปัญหาที่ 8 แต่อยู่ใน PermissionOrgController: -1. **Unhandled rejection**: หาก mapping fail ทั้ง batch จะ fail -2. **Complex nested ternary**: Logic ซับซ้อนเสี่ยงต่อ error -3. **No error boundary**: ไม่มี try-catch รอบๆ Promise.all - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -const data = await Promise.all( - record.map((_data) => { - const shortName = - _data.current_holders.length == 0 - ? null - : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != - null - ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` - : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) - ?.orgChild3 != null - ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` - : null; // ... logic ซับซ้อน - - return { /* ... */ }; - }), -); -``` - -### วิธีแก้ไขที่แนะนำ: -เหมือนปัญหาที่ 8 - ควรแยก logic ออกเป็น helper function และเพิ่ม error handling - ---- - -## 🟡 **ปัญหาที่ 11: Missing Error Handling ใน Delete Operations** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/ProfileAbilityController.ts` -- **บรรทัด:** 216-236 -- **Method:** `deleteProfileAbility` - -### ประเภทปัญหา: -2. **Missing Error Handle** - Delete Operation โดยไม่มี Transaction - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -การลบข้อมูล 2 ตารางต่อเนื่องกัน: -1. **Partial delete**: หากลบสำเร็จตารางแรก แต่ fail ตารางที่สอง ข้อมูลจะไม่สมบูรณ์ -2. **No rollback**: ไม่มี transaction ครอบ -3. **Orphaned records**: อาจมีข้อมูลที่เหลืออยู่โดยไม่มี parent - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -@Delete("{abilityId}") -public async deleteProfileAbility(@Path() abilityId: string, @Request() req: RequestWithUser) { - const _record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); - if (_record) { - await new permission().PermissionOrgUserDelete( - req, - "SYS_REGISTRY_OFFICER", - _record.profileId, - ); - } - await this.profileAbilityHistoryRepo.delete({ - profileAbilityId: abilityId, - }); - - const result = await this.profileAbilityRepo.delete({ id: abilityId }); - - if (result.affected == undefined || result.affected <= 0) - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - - return new HttpSuccess(); -} -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -@Delete("{abilityId}") -public async deleteProfileAbility(@Path() abilityId: string, @Request() req: RequestWithUser) { - try { - const _record = await this.profileAbilityRepo.findOneBy({ id: abilityId }); - if (_record) { - await new permission().PermissionOrgUserDelete( - req, - "SYS_REGISTRY_OFFICER", - _record.profileId, - ); - } else { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - } - - // ✅ ใช้ transaction - await AppDataSource.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(ProfileAbilityHistory, { - profileAbilityId: abilityId, - }); - - const result = await transactionalEntityManager.delete(ProfileAbility, { - id: abilityId, - }); - - if (result.affected == undefined || result.affected <= 0) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - } - }); - - return new HttpSuccess(); - } catch (error) { - if (error instanceof HttpError) { - throw error; - } - console.error('เกิดข้อผิดพลาดในการลบข้อมูล:', error); - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - "ไม่สามารถลบข้อมูลได้ กรุณาลองใหม่ในภายหลัง" - ); - } -} -``` - ---- - -## 🟡 **ปัญหาที่ 12: Null Reference ใน Map Operations** - -### ไฟล์และตำแหน่ง: -- **ไฟล์:** `src/controllers/PosMasterActController.ts` -- **บรรทัด:** 250-279 -- **Method:** `searchAct` - -### ประเภทปัญหา: -1. **Unhandled Exception** - Null Reference ใน Nested Optional Chaining - -### สาเหตุที่ทำให้เสี่ยงต่อการ Crash: -การเข้าถึง property ที่ซ้อนกันหลายชั้น: -1. **Complex optional chaining**: หาก intermediate value เป็น null อาจเกิด error -2. **Missing null checks**: บางจุดไม่ได้ใส่ optional chaining - -### โค้ดปัจจุบัน (มีปัญหา): -```typescript -const data = await Promise.all( - posMaster - .sort((a, b) => a.posMasterOrder - b.posMasterOrder) - .map((item) => { - const shortName = - item.orgChild4 != null - ? `${item.orgChild4.orgChild4ShortName} ${item.posMasterNo}` - : item?.orgChild3 != null - ? `${item.orgChild3.orgChild3ShortName} ${item.posMasterNo}` - : item?.orgChild2 != null - ? `${item.orgChild2.orgChild2ShortName} ${item.posMasterNo}` - : item?.orgChild1 != null - ? `${item.orgChild1.orgChild1ShortName} ${item.posMasterNo}` - : item?.orgRoot != null - ? `${item.orgRoot.orgRootShortName} ${item.posMasterNo}` - : null; - return { - id: item.id, - citizenId: item.current_holder?.citizenId ?? null, - // ... - }; - }), -); -``` - -### วิธีแก้ไขที่แนะนำ: -```typescript -// ✅ สร้าง helper function สำหรับ get short name -private getShortName(posMaster: any): string | null { - if (!posMaster) return null; - - if (posMaster.orgChild4?.orgChild4ShortName) { - return `${posMaster.orgChild4.orgChild4ShortName} ${posMaster.posMasterNo}`; - } - if (posMaster.orgChild3?.orgChild3ShortName) { - return `${posMaster.orgChild3.orgChild3ShortName} ${posMaster.posMasterNo}`; - } - if (posMaster.orgChild2?.orgChild2ShortName) { - return `${posMaster.orgChild2.orgChild2ShortName} ${posMaster.posMasterNo}`; - } - if (posMaster.orgChild1?.orgChild1ShortName) { - return `${posMaster.orgChild1.orgChild1ShortName} ${posMaster.posMasterNo}`; - } - if (posMaster.orgRoot?.orgRootShortName) { - return `${posMaster.orgRoot.orgRootShortName} ${posMaster.posMasterNo}`; - } - - return null; -} - -const data = await Promise.all( - posMaster - .sort((a, b) => a.posMasterOrder - b.posMasterOrder) - .map((item) => { - const shortName = this.getShortName(item); - - return { - id: item.id, - citizenId: item.current_holder?.citizenId ?? null, - isDirector: item.isDirector ?? null, - prefix: item.current_holder?.prefix ?? null, - firstName: item.current_holder?.firstName ?? null, - lastName: item.current_holder?.lastName ?? null, - posLevel: item.current_holder?.posLevel?.posLevelName ?? null, - posType: item.current_holder?.posType?.posTypeName ?? null, - position: item.current_holder?.position ?? null, - posNo: shortName, - }; - }), -); -``` - ---- - -## 📊 **สรุปสถิติ** - -| ระดับความรุนแรง | จำนวน | ประเภท | -|---|---|---| -| 🔴 วิกฤติ | 4 | มีโอกาสทำให้ Service Crash สูงมาก | -| 🟠 สูง | 5 | มีโอกาสทำให้เกิด Unhandled Exception | -| 🟡 ปานกลาง | 3 | อาจทำให้เกิดปัญหาในสถานการณ์เฉพาะ | -| **รวมทั้งหมด** | **12** | | - -### ไฟล์ที่มีปัญหามากที่สุด: -1. **PermissionController.ts** - 3 ปัญหา (รุนแรงที่สุด: Redis leak) -2. **PosMasterActController.ts** - 3 ปัญหา (Promise issues) -3. **PermissionOrgController.ts** - 2 ปัญหา -4. **PermissionProfileController.ts** - 2 ปัญหา -5. **PosTypeController.ts** - 1 ปัญหา -6. **ProfileAbilityController.ts** - 1 ปัญหา - ---- - -## 💡 **คำแนะนำเพื่อป้องกันปัญหาในอนาคต** - -### 1. ใช้ Redis Connection Pool -สร้าง singleton service สำหรับจัดการ Redis connection: -```typescript -export class RedisService { - private static client: any = null; - private static reconnectTimeout: NodeJS.Timeout | null = null; - - static async getClient() { - if (!this.client || !this.client.ready) { - await this.connect(); - } - return this.client; - } - - private static async connect() { - // Implementation with retry logic - } -} -``` - -### 2. Global Unhandled Rejection Handler -เพิ่มใน `main.ts` หรือ `app.ts`: -```typescript -process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled Rejection at:', promise, 'reason:', reason); - // อย่า crash ใน production แต่ log ไว้ debug - // process.exit(1); // ❌ อย่าทำใน production -}); - -process.on('uncaughtException', (error) => { - console.error('Uncaught Exception:', error); - // Clean up and restart - process.exit(1); // ✅ อาจ crash แต่ควร restart -}); -``` - -### 3. ใช้ Async Wrapper -สร้าง decorator หรือ helper function: -```typescript -export function asyncHandler(fn: Function) { - return (req: any, res: any, next: any) => { - Promise.resolve(fn(req, res, next)).catch(next); - }; -} - -// ใช้งาน -@Get() -asyncHandler(async (request: RequestWithUser) => { - // ... logic -}); -``` - -### 4. ตรวจสอบ ESLint Rules -เพิ่ม rules เหล่านี้ใน `.eslintrc.json`: -```json -{ - "rules": { - "no-throw-literal": "error", - "require-await": "error", - "no-return-await": "off", - "prefer-promise-reject-errors": "error" - } -} -``` - -### 5. เขียน Integration Tests -ทดสอบ error scenarios: -- Redis connection failures -- Database constraint violations -- Concurrent updates -- Memory pressure - -### 6. Monitoring -ติดตั้ง monitoring tools: -- Track Redis connection count -- Monitor memory usage -- Log unhandled rejections -- Set up alerts for crash loops - ---- - -## 📝 **บันทึกเพิ่มเติม** - -รายงานนี้ครอบคลุมการวิเคราะห์ **ชุดที่ 5** ซึ่งประกอบด้วย 10 Controllers: - -1. PermissionController.ts ⚠️ **มีปัญหารุนแรง (Redis Leak)** -2. PermissionOrgController.ts -3. PermissionProfileController.ts -4. PosExecutiveController.ts -5. PosLevelController.ts -6. PosMasterActController.ts ⚠️ **มีปัญหา Promise Handling** -7. PosTypeController.ts -8. PositionController.ts -9. PrefixController.ts -10. ProfileAbilityController.ts - -**วันที่สร้างรายงาน:** 8 พฤษภาคม 2568 -**เครื่องมือที่ใช้:** การวิเคราะห์ Code และ Pattern Recognition diff --git a/reports/batch-06-controllers-51-60-analysis.md b/reports/batch-06-controllers-51-60-analysis.md deleted file mode 100644 index 92318520..00000000 --- a/reports/batch-06-controllers-51-60-analysis.md +++ /dev/null @@ -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 ในทันที** แต่ควรปรับปรุงตามคำแนะนำเพื่อเพิ่มความเสถียรของระบบในระยะยาว diff --git a/reports/batch-07-controllers-61-70-analysis.md b/reports/batch-07-controllers-61-70-analysis.md deleted file mode 100644 index 7dde85e7..00000000 --- a/reports/batch-07-controllers-61-70-analysis.md +++ /dev/null @@ -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 ในทันที** แต่ควรปรับปรุงตามคำแนะนำเพื่อเพิ่มความเสถียรของระบบในระยะยาว diff --git a/reports/batch-08-controllers-71-80-analysis.md b/reports/batch-08-controllers-71-80-analysis.md deleted file mode 100644 index 790197db..00000000 --- a/reports/batch-08-controllers-71-80-analysis.md +++ /dev/null @@ -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 diff --git a/reports/batch-09-controllers-81-90-analysis.md b/reports/batch-09-controllers-81-90-analysis.md deleted file mode 100644 index 69a39cb6..00000000 --- a/reports/batch-09-controllers-81-90-analysis.md +++ /dev/null @@ -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 diff --git a/reports/batch-10-controllers-91-100-analysis.md b/reports/batch-10-controllers-91-100-analysis.md deleted file mode 100644 index 4dd7b172..00000000 --- a/reports/batch-10-controllers-91-100-analysis.md +++ /dev/null @@ -1,1070 +0,0 @@ -# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop -## Batch 10: Controllers 91-100 - -**วันที่ตรวจสอบ:** 2026-05-08 -**จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers - ---- - -## Controllers ที่ตรวจสอบในชุดนี้ - -1. [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts) -2. [SocketController.ts](src/controllers/SocketController.ts) -3. [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts) -4. [ApiManageController.ts](src/controllers/ApiManageController.ts) -5. [ImportDataController.ts](src/controllers/ImportDataController.ts) -6. [ExRetirementController.ts](src/controllers/ExRetirementController.ts) -7. [IssuesController.ts](src/controllers/IssuesController.ts) -8. [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts) -9. [MyController.ts](src/controllers/MyController.ts) -10. [MainController.ts](src/controllers/MainController.ts) - ---- - -## รายการปัญหาที่พบ - -### 1. 🔴 CRITICAL - KeycloakSyncController.ts - Unhandled Promise in Loop - -**File & Location:** [KeycloakSyncController.ts](src/controllers/KeycloakSyncController.ts:159-182) - `syncByProfileIds()` method - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -```typescript -for (const profileId of profileIds) { - try { - const success = await this.keycloakAttributeService.syncOnOrganizationChange( - profileId, - profileType, - ); - // ... - } catch (error: any) { - result.failed++; - result.details.push({ profileId, status: "failed", error: error.message }); - } -} -``` - -แม้ว่าจะมี `try-catch` ภายใน loop แต่การ catch error แล้วเพียงแค่บันทึกผลลัพธ์ อาจไม่เพียงพอสำหรับบางกรณี: -- หาก `syncOnOrganizationChange` มี Promise rejection ที่ไม่ถูก handle อย่างถูกต้องภายใน service -- หากเกิด error ระหว่างการทำงานของ loop ที่ไม่ใช่จาก `syncOnOrganizationChange` เช่น จาก `result.details.push()` -- Error ที่เกิดขึ้นอาจเป็น unhandled rejection หาก service ไม่ return Promise อย่างถูกต้อง - -**Recommended Fix:** -```typescript -@Post("sync-profiles-batch") -async syncByProfileIds( - @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" }, -) { - const { profileIds, profileType } = request; - - if (!profileIds || profileIds.length === 0) { - throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า"); - } - - if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) { - throw new HttpError( - HttpStatus.BAD_REQUEST, - "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น", - ); - } - - const result = { - total: profileIds.length, - success: 0, - failed: 0, - details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>, - }; - - // เพิ่ม timeout protection และ error handling ที่ดีขึ้น - const SYNC_TIMEOUT = 30000; // 30 วินาทีต่อ profile - - for (const profileId of profileIds) { - try { - // เพิ่ม Promise.race เพื่อป้องกันการ hang - const syncPromise = this.keycloakAttributeService.syncOnOrganizationChange( - profileId, - profileType, - ); - - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Sync timeout')), SYNC_TIMEOUT) - ); - - const success = await Promise.race([syncPromise, timeoutPromise]) as boolean; - - if (success) { - result.success++; - result.details.push({ profileId, status: "success" }); - } else { - result.failed++; - result.details.push({ - profileId, - status: "failed", - error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID", - }); - } - } catch (error: any) { - result.failed++; - // เพิ่ม validation ก่อน push เพื่อป้องกัน crash จาก invalid data - const errorMessage = error?.message || String(error); - result.details.push({ - profileId, - status: "failed", - error: errorMessage.substring(0, 500) // จำกัดความยาว - }); - - // Log error สำหรับ monitoring - console.error(`[KeycloakSync] Failed to sync profile ${profileId}:`, error); - } - } - - return new HttpSuccess({ - message: "Batch sync เสร็จสิ้น", - ...result, - }); -} -``` - ---- - -### 2. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Large Loop - -**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:219-364) - `UploadFileSqlOfficer()` method - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -```typescript -@Post("uploadProfile-Officer") -async UploadFileSqlOfficer(@Request() request: { user: Record }) { - const OFFICER = await this.OFFICERRepo.find(); - let rowCount = 0; - // ... - for await (const item of OFFICER) { - rowCount++; - // ... การประมวลผลข้อมูล ... - await this.profileRepo.save(profile); - } - return new HttpSuccess(); -} -``` - -**ปัญหาที่พบ:** -1. **ไม่มี try-catch รอบ loop** - หากเกิด error ระหว่างการประมวลผล เช่น: - - Database connection lost - - Invalid data format - - Constraint violation - - Memory overflow - - จะทำให้เกิด Unhandled Exception และ **Process Crash** - -2. **ไม่มี Error Recovery** - หากเกิด error ที่ record ใด record หนึ่ง ทั้งกระบวนการจะหยุดทันที และไม่มีการ rollback หรือ cleanup - -3. **Loading all data at once** - `await this.OFFICERRepo.find()` โหลดข้อมูลทั้งหมดเข้า memory อาจทำให้เกิด Out of Memory - -4. **No transaction management** - แต่ละรอบบันทึกแยกกัน หากเกิด error ข้อมูลบางส่วนอาจถูกบันทึกแล้วบางส่วนไม่ได้ - -**Recommended Fix:** -```typescript -@Post("uploadProfile-Officer") -async UploadFileSqlOfficer(@Request() request: { user: Record }) { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - let rowCount = 0; - let successCount = 0; - let failedCount = 0; - const errors: Array<{row: number, citizenId: string, error: string}> = []; - - try { - // ใช้ pagination แทนการโหลดทั้งหมด - const BATCH_SIZE = 500; - let offset = 0; - let hasMore = true; - - while (hasMore) { - const OFFICER = await queryRunner.manager.find(OFFICER, { - take: BATCH_SIZE, - skip: offset, - order: { id: 'ASC' } - }); - - if (OFFICER.length === 0) { - hasMore = false; - break; - } - - for (const item of OFFICER) { - rowCount++; - - try { - let type_: any = null; - let level_: any = null; - const profile = new Profile(); - - const existingProfile = await queryRunner.manager.findOne(Profile, { - where: { citizenId: item.CIT.toString() }, - }); - - if (existingProfile) { - // ข้ามกรณีมีข้อมูลอยู่แล้ว - continue; - } - - // ... การประมวลผลข้อมูลเดิม ... - - // ใช้ queryRunner.manager.save แทน this.profileRepo.save - await queryRunner.manager.save(profile); - successCount++; - - } catch (itemError: any) { - failedCount++; - errors.push({ - row: rowCount, - citizenId: item.CIT?.toString() || 'unknown', - error: itemError?.message || String(itemError) - }); - // Log แต่ไม่หยุดการทำงาน - console.error(`[UploadOfficer] Error at row ${rowCount}:`, itemError); - } - } - - offset += BATCH_SIZE; - - // Commit ทุกๆ batch เพื่อป้องกัน transaction ใหญ่เกินไป - await queryRunner.commitTransaction(); - await queryRunner.startTransaction(); - } - - // Commit transaction สุดท้าย - await queryRunner.commitTransaction(); - - return new HttpSuccess({ - message: "อัปโหลดข้อมูลเสร็จสิ้น", - total: rowCount, - success: successCount, - failed: failedCount, - errors: errors.slice(0, 100) // ส่งเฉพาะ 100 errors แรก - }); - - } catch (error: any) { - await queryRunner.rollbackTransaction(); - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - `ไม่สามารถอัปโหลดข้อมูลได้: ${error?.message || 'Unknown error'}` - ); - } finally { - await queryRunner.release(); - } -} -``` - ---- - -### 3. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Employee Upload - -**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:369-496) - `UploadFileSQL()` method - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -เหมือนกับปัญหาข้างต้น แต่สำหรับการอัปโหลดข้อมูลลูกจ้างประจำ มีความเสี่ยงเช่นเดียวกัน: -- ไม่มี try-catch ใน loop -- ไม่มี transaction management -- ไม่มี error recovery - -**Recommended Fix:** -ใช้ pattern เดียวกับข้อ 2 โดยใช้ QueryRunner สำหรับ transaction management - ---- - -### 4. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Temp Employee Upload - -**File & Location:** [ImportDataController.ts](src/controllers/ImportDataController.ts:501-633) - `UploadFileSQLTemp()` method - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -```typescript -if (item.CIT.toString() == "1101801164891") { - continue; -} -const existingProfile = await this.profileEmpRepo.findOne({ - where: { employeeClass: "TEMP", citizenId: item.CIT.toString() }, -}); -if (existingProfile) { - profile.id = existingProfile.id; -} else { - continue; -} -``` - -**ปัญหาเพิ่มเติม:** -1. **Hardcoded citizenId check** - มีการ hardcode เงื่อนไข `item.CIT.toString() == "1101801164891"` ซึ่งอาจเป็น bug หรือ test code ที่ลืมลบ -2. **การ skip ที่ไม่ชัดเจน** - หากไม่พบ existingProfile จะ continue ทันที ทำให้ไม่สร้าง profile ใหม่ -3. **ไม่มี error handling** เหมือนปัญหาก่อนหน้า - -**Recommended Fix:** -```typescript -@Post("uploadProfile-EmployeeTemp") -async UploadFileSQLTemp(@Request() request: { user: Record }) { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - let rowCount = 0; - let successCount = 0; - let failedCount = 0; - const errors: Array<{row: number, citizenId: string, error: string}> = []; - - try { - const BATCH_SIZE = 500; - let offset = 0; - let hasMore = true; - - while (hasMore) { - const EMPLOYEE = await queryRunner.manager.find(EMPLOYEETEMP, { - take: BATCH_SIZE, - skip: offset, - order: { id: 'ASC' } - }); - - if (EMPLOYEE.length === 0) { - hasMore = false; - break; - } - - for (const item of EMPLOYEE) { - rowCount++; - - try { - // เอา hardcode check ออก หรือเปลี่ยนเป็น configurable - // if (item.CIT.toString() === "1101801164891") { - // continue; - // } - - const existingProfile = await queryRunner.manager.findOne(ProfileEmployee, { - where: { - employeeClass: "TEMP", - citizenId: item.CIT.toString() - }, - }); - - let profile: ProfileEmployee; - - if (existingProfile) { - profile = existingProfile; - } else { - // สร้าง profile ใหม่ถ้าไม่พบ - profile = new ProfileEmployee(); - profile.employeeClass = "TEMP"; - } - - // ... การประมวลผลข้อมูลเดิม ... - - await queryRunner.manager.save(profile); - successCount++; - - } catch (itemError: any) { - failedCount++; - errors.push({ - row: rowCount, - citizenId: item.CIT?.toString() || 'unknown', - error: itemError?.message || String(itemError) - }); - console.error(`[UploadEmployeeTemp] Error at row ${rowCount}:`, itemError); - } - } - - offset += BATCH_SIZE; - await queryRunner.commitTransaction(); - await queryRunner.startTransaction(); - } - - await queryRunner.commitTransaction(); - - return new HttpSuccess({ - message: "อัปโหลดข้อมูลลูกจ้างชั่วคราวเสร็จสิ้น", - total: rowCount, - success: successCount, - failed: failedCount, - errors: errors.slice(0, 100) - }); - - } catch (error: any) { - await queryRunner.rollbackTransaction(); - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - `ไม่สามารถอัปโหลดข้อมูลลูกจ้างชั่วคราวได้: ${error?.message || 'Unknown error'}` - ); - } finally { - await queryRunner.release(); - } -} -``` - ---- - -### 5. 🟡 HIGH - ExRetirementController.ts - Unhandled External API Error - -**File & Location:** [ExRetirementController.ts](src/controllers/ExRetirementController.ts:148-173) - `getToken()` function - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -async function getToken(ClientID: string, ClientSecret: string): Promise { - // ... - 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 }); - } -} -``` - -**ปัญหา:** -1. **Generic error handling** - Error ที่ return มาเป็น object ธรรมดา ไม่ใช่ Error instance ทำให้การ stack trace หายไป -2. **ไม่มี retry logic** - หาก external API ล้ม ชั่วคราว จะไม่มีการ retry อัตโนมัติ -3. **No timeout** - หาก external API ไม่ตอบสนอง จะทำให้ request ค้างไปตลอด - -**Recommended Fix:** -```typescript -async function getToken(ClientID: string, ClientSecret: string): Promise { - const cacheKey = `${ClientID}:${ClientSecret}`; - - const cachedToken = TokenCache.get(cacheKey); - if (cachedToken) { - return cachedToken; - } - - const MAX_RETRIES = 3; - const TIMEOUT = 10000; // 10 วินาที - let lastError: any; - - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - 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: TIMEOUT, - }); - - const token = res.data.token; - if (!token) { - throw new Error('Token not found in response'); - } - - TokenCache.set(cacheKey, token); - return token; - - } catch (error: any) { - lastError = error; - - // ไม่ retry หากเป็น client error (4xx) - if (error.response?.status >= 400 && error.response?.status < 500) { - break; - } - - // Retry หากเป็น server error หรือ network error - if (attempt < MAX_RETRIES) { - const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } - - // Log error สำหรับ monitoring - console.error(`[ExRetirement] Failed to get token after ${MAX_RETRIES} attempts:`, lastError); - - throw new Error(`ไม่สามารถขอ Token ได้: ${lastError?.message || 'Unknown error'}`); -} -``` - ---- - -### 6. 🟡 HIGH - ApiWebServiceController.ts - Potential Null Reference - -**File & Location:** [ApiWebServiceController.ts](src/controllers/ApiWebServiceController.ts:67-78) - `listAttribute()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -if (system == "organization") { - tbMain = "OrgRoot"; - const revision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - condition = `OrgRoot.orgRevisionId = "${revision?.id}"`; -} -``` - -**ปัญหา:** -1. **revision อาจเป็น null** - หากไม่พบ revision ที่ตรงตามเงื่อนไข `revision?.id` จะเป็น `undefined` -2. **SQL Injection vulnerability** - การใส่ค่าโดยตรงเข้าไปใน condition string อาจทำให้เกิด SQL injection หรือ syntax error -3. **ไม่มี error handling** - หาก query ล้มเพราะ invalid condition จะทำให้เกิด unhandled exception - -**Recommended Fix:** -```typescript -@Get("/:system/:code") -async listAttribute( - @Request() request: RequestWithUserWebService, - @Path("system") - system: SystemCode, - @Path("code") code: string, - @Query("page") page: number = 1, - @Query("pageSize") pageSize: number = 100, -): Promise { - try { - const apiName = await this.apiNameRepository.findOne({ - where: { code }, - select: ["id", "code", "methodApi", "system", "isActive"], - relations: ["apiAttributes"], - order: { - apiAttributes: { - ordering: "ASC", - }, - }, - }); - - if (!apiName || apiName.system != system || !apiName.isActive || apiName.methodApi != "GET") { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบ API ที่ระบุ"); - } - - await isPermissionRequest(request, apiName.id); - - const offset = (page - 1) * pageSize; - const propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`); - - let tbMain: string = ""; - let condition: string = "1=1"; - let revisionId: string | null = null; - - if (system == "registry") { - tbMain = "Profile"; - } else if (system == "registry_emp") { - tbMain = "ProfileEmployee"; - condition = `ProfileEmployee.employeeClass = "PERM"`; - } else if (system == "registry_temp") { - tbMain = "ProfileEmployee"; - condition = `ProfileEmployee.employeeClass = "TEMP"`; - } else if (system == "organization") { - tbMain = "OrgRoot"; - const revision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - if (!revision) { - throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน"); - } - - revisionId = revision.id; - condition = `OrgRoot.orgRevisionId = :revisionId`; - } else if (system == "position") { - tbMain = "PosMaster"; - const revision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - if (!revision) { - throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน"); - } - - revisionId = revision.id; - condition = `PosMaster.orgRevisionId = :revisionId`; - } - - const repo = AppDataSource.getRepository(tbMain); - const metadata = repo.metadata; - - const relationMap: Record = {}; - metadata.relations.forEach((rel) => { - relationMap[rel.inverseEntityMetadata.name] = rel.propertyName; - }); - - let propertyOtherKey: any[] = []; - propertyOtherKey = [ - ...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)), - ]; - - const queryBuilder = repo.createQueryBuilder(tbMain); - - if (propertyOtherKey.length > 0) { - propertyOtherKey.forEach((tb) => { - const relationName = relationMap[tb]; - if (relationName) { - queryBuilder.leftJoin( - `${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`, - tb, - ); - } - }); - } - - let pk: string = ""; - const primaryColumns = metadata.primaryColumns; - primaryColumns.forEach((col) => { - pk = col.propertyName; - if (!propertyKey.includes(`${tbMain}.${pk}`)) { - propertyKey.push(`${tbMain}.${pk}`); - } - }); - - // ใช้ parameterized query แทน string interpolation - const queryParams: any = {}; - if (revisionId) { - queryParams.revisionId = revisionId; - } - - const [items, total] = await queryBuilder - .select(propertyKey) - .where(condition, queryParams) - .orderBy(propertyKey[0], "ASC") - .skip(offset) - .take(pageSize) - .getManyAndCount(); - - const data = items.map((item) => { - const { [pk]: removedPk, ...x } = item; - return x; - }); - - // save api history after query success - const history = { - headerApi: JSON.stringify({ - host: request.headers.host, - "x-api-key": request.headers["x-api-key"], - connection: request.headers.connection, - accept: request.headers.accept, - }), - tokenApi: Array.isArray(request.headers["x-api-key"]) - ? request.headers["x-api-key"][0] || "" - : request.headers["x-api-key"] || "", - requestApi: `${request.method} ${request.protocol}://${request.headers.host}${request.originalUrl || request.url}`, - responseApi: "OK", - ipApi: request.ip, - codeApi: code, - apiKeyId: request.user.id, - apiNameId: apiName.id, - createdFullName: request.user.name, - lastUpdateFullName: request.user.name, - }; - - try { - await this.apiHistoryRepository.save(history); - } catch (historyError) { - // Log แต่ไม่ให้กระทบต่อ response - console.error('[ApiWebService] Failed to save history:', historyError); - } - - return new HttpSuccess({ data: data, total }); - - } catch (error: any) { - if (error instanceof HttpError) { - throw error; - } - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - `เกิดข้อผิดพลาด: ${error?.message || 'Unknown error'}` - ); - } -} -``` - ---- - -### 7. 🟡 MEDIUM - ApiManageController.ts - Missing Transaction Error Handling - -**File & Location:** [ApiManageController.ts](src/controllers/ApiManageController.ts:464-518) - `createApi()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -@Post("") -async createApi( - @Request() req: RequestWithUser, - @Body() apiData: CreateApi, -): Promise { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - this.validateSuperAdminRole(req.user); - // ... - await queryRunner.commitTransaction(); - return new HttpSuccess(apiName.id); - } catch (error) { - await queryRunner.rollbackTransaction(); - throw new HttpError(...); - } finally { - await queryRunner.release(); - } -} -``` - -**ปัญหา:** -1. **validateSuperAdminRole อยู่นอก try-catch** - หาก function นี้ throw error จะทำให้ queryRunner ไม่ถูก release และเกิด connection leak -2. **ไม่ validate req.user** ก่อนเรียก `validateSuperAdminRole` - หาก `req.user` เป็น null หรือ undefined จะเกิด error - -**Recommended Fix:** -```typescript -@Post("") -async createApi( - @Request() req: RequestWithUser, - @Body() apiData: CreateApi, -): Promise { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - // Validate request ก่อน - if (!req.user) { - throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่พบข้อมูลผู้ใช้"); - } - - this.validateSuperAdminRole(req.user); - - const code = this.generateApiCode(); - const postData = { - name: apiData.name, - code, - pathApi: this.createApiPath(apiData.system as SystemCode, code), - methodApi: apiData.methodApi || "GET", - system: apiData.system || "registry", - isActive: apiData.isActive || false, - createdUserId: req.user?.sub, - createdFullName: req.user?.name || "", - }; - - const apiName = await queryRunner.manager.getRepository(ApiName).save(postData); - - if (apiData.apiAttributes?.length) { - let orderingCounter = 0; - const attributesToSave = apiData.apiAttributes.flatMap((attr) => - attr.propertyKey.map((propertyKey) => ({ - apiNameId: apiName.id, - tbName: attr.tbName, - propertyKey, - ordering: orderingCounter++, - createdUserId: req.user?.sub, - createdFullName: req.user?.name || "", - })), - ); - - await queryRunner.manager.getRepository(ApiAttribute).save(attributesToSave); - } - - await queryRunner.commitTransaction(); - return new HttpSuccess(apiName.id); - } catch (error) { - await queryRunner.rollbackTransaction(); - - if (error instanceof HttpError) { - throw error; - } - - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - error instanceof Error ? error.message : "เกิดข้อผิดพลาด ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่ในภายหลัง", - ); - } finally { - // Ensure release is called even if rollback fails - try { - await queryRunner.release(); - } catch (releaseError) { - console.error('[ApiManage] Failed to release queryRunner:', releaseError); - } - } -} -``` - ---- - -### 8. 🟡 MEDIUM - DevelopmentRequestController.ts - Unhandled Promise in Parallel Operations - -**File & Location:** [DevelopmentRequestController.ts](src/controllers/DevelopmentRequestController.ts:349-365) - `newDevelopmentRequest()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```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.lastUpdateUpdatedAt = new Date(); - developmentProject.developmentRequestId = data.id; - await this.developmentProjectRepository.save(developmentProject, { data: req }); - setLogDataDiff(req, { before, after: developmentProject }); - }), - ); -} -``` - -**ปัญหา:** -1. **Unhandled Promise rejection** - หาก `save` หรือ `setLogDataDiff` ล้ม จะเกิด unhandled rejection -2. **ไม่มี error handling รายตัว** - หาก project หนึ่งล้ม ทั้ง batch จะล้ม -3. **No cleanup on partial failure** - หาก save บางส่วนสำเร็จแล้วล้ม จะมีข้อมูล partial อยู่ใน database - -**Recommended Fix:** -```typescript -if (body.developmentProjects != null) { - const savedProjects: DevelopmentProject[] = []; - - try { - for (const projectName of body.developmentProjects) { - try { - let developmentProject = new DevelopmentProject(); - developmentProject.name = projectName; - 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.lastUpdateUpdatedAt = new Date(); - developmentProject.developmentRequestId = data.id; - - const saved = await this.developmentProjectRepository.save(developmentProject, { data: req }); - savedProjects.push(saved); - - setLogDataDiff(req, { before: null, after: saved }); - } catch (projectError: any) { - console.error(`[DevelopmentRequest] Failed to save project ${projectName}:`, projectError); - // Continue with next project instead of failing entire request - } - } - } catch (batchError: any) { - console.error('[DevelopmentRequest] Error in projects batch:', batchError); - // Clean up any successfully saved projects if needed - if (savedProjects.length > 0) { - try { - await this.developmentProjectRepository.delete({ - developmentRequestId: data.id - }); - } catch (cleanupError) { - console.error('[DevelopmentRequest] Failed to cleanup projects:', cleanupError); - } - } - throw batchError; - } -} -``` - ---- - -### 9. 🟢 LOW - SocketController.ts - No Error Handling - -**File & Location:** [SocketController.ts](src/controllers/SocketController.ts:6-24) - `notify()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -@Post("notify") -async notify( - @Body() - payload: { - message: string; - userId?: string | string[]; - roles?: string | string[]; - error?: boolean; - }, -) { - sendWebSocket( - "socket-notification", - { success: !payload.error, message: payload.message }, - { - roles: payload.roles || [], - userId: payload.userId || [], - }, - ); -} -``` - -**ปัญหา:** -1. **ไม่มี try-catch** - หาก `sendWebSocket` throw error จะเป็น unhandled exception -2. **ไม่ return ค่า** - ไม่มีการ return HttpSuccess หรือ error response -3. **No validation** - ไม่ validate payload ก่อนใช้งาน - -**Recommended Fix:** -```typescript -@Post("notify") -async notify( - @Body() - payload: { - message: string; - userId?: string | string[]; - roles?: string | string[]; - error?: boolean; - }, -) { - try { - // Validate payload - if (!payload.message || typeof payload.message !== 'string') { - throw new HttpError(HttpStatus.BAD_REQUEST, "message ต้องเป็น string ที่ไม่ว่างเปล่า"); - } - - // Validate userId and roles - if (payload.userId && !Array.isArray(payload.userId) && typeof payload.userId !== 'string') { - throw new HttpError(HttpStatus.BAD_REQUEST, "userId ต้องเป็น string หรือ array of strings"); - } - - if (payload.roles && !Array.isArray(payload.roles) && typeof payload.roles !== 'string') { - throw new HttpError(HttpStatus.BAD_REQUEST, "roles ต้องเป็น string หรือ array of strings"); - } - - sendWebSocket( - "socket-notification", - { success: !payload.error, message: payload.message }, - { - roles: payload.roles || [], - userId: payload.userId || [], - }, - ); - - return new HttpSuccess({ - message: "ส่งการแจ้งเตือนสำเร็จ", - notification: { - type: "socket-notification", - success: !payload.error, - message: payload.message, - roles: payload.roles || [], - userId: payload.userId || [], - } - }); - } catch (error: any) { - if (error instanceof HttpError) { - throw error; - } - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `ไม่สามารถส่งการแจ้งเตือนได้: ${error?.message || 'Unknown error'}` - ); - } -} -``` - ---- - -### 10. 🟢 LOW - IssuesController.ts - Missing Try-Catch - -**File & Location:** [IssuesController.ts](src/controllers/IssuesController.ts:31-39) - `getIssues()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -@Get("lists") -async getIssues() { - const issues = await this.issuesRepository.find({ - order: { - createdAt: "DESC", - }, - }); - return new HttpSuccess(issues); -} -``` - -**ปัญหา:** -- ไม่มี try-catch หาก database connection ล้มหรือ query มีปัญหา จะเกิด unhandled exception - -**Recommended Fix:** -```typescript -@Get("lists") -async getIssues() { - try { - const issues = await this.issuesRepository.find({ - order: { - createdAt: "DESC", - }, - }); - return new HttpSuccess(issues); - } catch (error: any) { - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - `ไม่สามารถดึงรายการปัญหาได้: ${error?.message || 'Unknown error'}` - ); - } -} -``` - ---- - -## สรุปสถิติ - -| ระดับความรุนแรง | จำนวน | รายการ | -|---|---|---| -| 🔴 CRITICAL | 4 | 1, 2, 3, 4 | -| 🟡 HIGH | 2 | 5, 6 | -| 🟡 MEDIUM | 2 | 7, 8 | -| 🟢 LOW | 2 | 9, 10 | - ---- - -## คำแนะนำการจัดลำดับการแก้ไข - -### แก้ไขทันที (P0 - Critical) -1. **ImportDataController.ts** - ทั้ง 3 methods (`UploadFileSqlOfficer`, `UploadFileSQL`, `UploadFileSQLTemp`) - - เพิ่ม transaction management - - เพิ่ม try-catch ใน loops - - เพิ่ม pagination แทนการโหลดทั้งหมด - -### แก้ไขเร็วๆ นี้ (P1 - High) -2. **KeycloakSyncController.ts** - เพิ่ม timeout protection -3. **ExRetirementController.ts** - ปรับปรุง error handling และ retry logic -4. **ApiWebServiceController.ts** - แก้ null reference issue - -### แก้ไขในภายหลัง (P2 - Medium) -5. **ApiManageController.ts** - ปรับปรุง transaction error handling -6. **DevelopmentRequestController.ts** - เพิ่ม error handling สำหรับ parallel operations - -### แก้ไขเมื่อว่าง (P3 - Low) -7. **SocketController.ts** - เพิ่ม validation และ error handling -8. **IssuesController.ts** - เพิ่ม try-catch - ---- - -## ข้อเสนอแนะเพิ่มเติม - -1. **ใช้ Global Error Handler** - ให้พิจารณาใช้ TSOA's middleware หรือ NestJS interceptor สำหรับ centralized error handling -2. **เพิ่ม Health Check** - สำหรับ endpoints ที่เชื่อมต่อกับ external services (Keycloak, ExProfile API) -3. **Circuit Breaker Pattern** - สำหรับการเรียก external API เพื่อป้องกัน cascade failures -4. **Graceful Shutdown** - ให้แน่ใจว่า long-running operations สามารถยกเลิกได้อย่างปลอดภัยเมื่อ server shutdown -5. **Logging Strategy** - เพิ่ม structured logging สำหรับ monitoring และ debugging - ---- - -## ไฟล์รายงานที่เกี่ยวข้อง - -- [Batch 1-10 Analysis](../reports/) - รายงานการตรวจสอบ Controllers ชุดก่อนหน้า -- [Security Audit Report](../reports/security-audit.md) - รายงานการตรวจสอบด้านความปลอดภัย diff --git a/reports/batch-11-controllers-101-110-analysis.md b/reports/batch-11-controllers-101-110-analysis.md deleted file mode 100644 index 85a60ecd..00000000 --- a/reports/batch-11-controllers-101-110-analysis.md +++ /dev/null @@ -1,1160 +0,0 @@ -# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop -## Batch 11: Controllers 101-110 - -**วันที่ตรวจสอบ:** 2026-05-08 -**จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers - ---- - -## Controllers ที่ตรวจสอบในชุดนี้ - -1. [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts) -2. [ReportController.ts](src/controllers/ReportController.ts) - เกินขนาด 256KB (skip) -3. [ScriptProfileOrgController.ts](src/controllers/ScriptProfileOrgController.ts) -4. [WorkflowController.ts](src/controllers/WorkflowController.ts) -5. [ProfileTrainingController.ts](src/controllers/ProfileTrainingController.ts) -6. [OrganizationController.ts](src/controllers/OrganizationController.ts) - ตรวจสอบแล้วใน batch ก่อนหน้า -7. [PositionController.ts](src/controllers/PositionController.ts) - ตรวจสอบแล้วใน batch ก่อนหน้า -8. [ProfileController.ts](src/controllers/ProfileController.ts) - ตรวจสอบแล้วใน batch ก่อนหน้า -9. [CommandController.ts](src/controllers/CommandController.ts) - ตรวจสอบแล้วใน batch ก่อนหน้า -10. [User Controller.ts](src/controllers/UserController.ts) - ตรวจสอบแล้วใน batch ก่อนหน้า - ---- - -## รายการปัญหาที่พบ - -### 1. 🔴 CRITICAL - ProfileSalaryTempController.ts - Unhandled Exception in Large Loop - -**File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts:1725-1747) - `changeSortEditGenAll()` method - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -```typescript -@Get("change/sort/all") -public async changeSortEditGenAll() { - try { - const profiles = await this.profileRepo.find(); - let num = 1; - 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); - } - - return new HttpSuccess(); - } catch { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่สามารถดำเนินการได้"); - } -} -``` - -**ปัญหาที่พบ:** -1. **Loading all profiles at once** - `await this.profileRepo.find()` โหลดข้อมูลทั้งหมดเข้า memory อาจทำให้เกิด Out of Memory -2. **No transaction management** - แต่ละรอบบันทึกแยกกัน หากเกิด error ข้อมูลบางส่วนอาจถูกบันทึกแล้วบางส่วนไม่ได้ -3. **No error handling per iteration** - หากเกิด error ใน loop ที่ profile ใด profile หนึ่ง ทั้งกระบวนการจะหยุดทันที -4. **Generic catch block** - catch แล้ว throw generic error โดยไม่ log รายละเอียดของ error ทำให้ debug ยาก -5. **No timeout protection** - หากมี profiles จำนวนมาก อาจทำให้ request ค้างนานเกินไป - -**Recommended Fix:** -```typescript -@Get("change/sort/all") -public async changeSortEditGenAll() { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - let processedCount = 0; - let failedCount = 0; - const errors: Array<{profileId: string, error: string}> = []; - - try { - const BATCH_SIZE = 500; - let offset = 0; - let hasMore = true; - - while (hasMore) { - const profiles = await queryRunner.manager.find(Profile, { - select: ["id"], - take: BATCH_SIZE, - skip: offset, - order: { id: 'ASC' } - }); - - if (profiles.length === 0) { - hasMore = false; - break; - } - - for (const profile of profiles) { - try { - const salaryOld = await queryRunner.manager.find(ProfileSalary, { - where: { profileId: profile.id }, - order: { commandDateAffect: "ASC", order: "ASC" }, - }); - - // Update order - for (let i = 0; i < salaryOld.length; i++) { - salaryOld[i].order = i + 1; - } - - await queryRunner.manager.save(salaryOld); - processedCount++; - - if (processedCount % 100 === 0) { - console.log(`Processed ${processedCount} profiles`); - } - } catch (itemError: any) { - failedCount++; - errors.push({ - profileId: profile.id, - error: itemError?.message || String(itemError) - }); - console.error(`Failed to process profile ${profile.id}:`, itemError); - } - } - - // Commit per batch to avoid large transaction - await queryRunner.commitTransaction(); - await queryRunner.startTransaction(); - - offset += BATCH_SIZE; - } - - await queryRunner.commitTransaction(); - - return new HttpSuccess({ - message: "ปรับปรุงลำดับเสร็จสิ้น", - processed: processedCount, - failed: failedCount, - errors: errors.slice(0, 100) // ส่งเฉพาะ 100 errors แรก - }); - - } catch (error: any) { - await queryRunner.rollbackTransaction(); - console.error("changeSortEditGenAll failed:", error); - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - `ไม่สามารถดำเนินการได้: ${error?.message || 'Unknown error'}` - ); - } finally { - await queryRunner.release(); - } -} -``` - ---- - -### 2. 🔴 CRITICAL - ScriptProfileOrgController.ts - Unhandled External API Error - -**File & Location:** [ScriptProfileOrgController.ts](src/controllers/ScriptProfileOrgController.ts:184-190) - `cronjobUpdateOrg()` method - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -```typescript -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 -}); -``` - -**ปัญหาที่พบ:** -1. **No error handling for external API** - หาก external API ล้มหรือ timeout จะเกิด unhandled exception -2. **No retry logic** - ไม่มีการ retry หาก external API ล้มชั่วคราว -3. **No validation of environment variables** - `process.env.API_URL` หรือ `process.env.API_KEY` อาจเป็น undefined -4. **Payload size not validated** - หาก payloads ขนาดใหญ่เกินไปอาจทำให้ external API ล้ม -5. **Circuit breaker not implemented** - หาก external API ล้มต่อเนื่อง จะไม่มีการหยุดชั่วคราว - -**Recommended Fix:** -```typescript -// Add at class level -private apiFailureCount = 0; -private readonly API_FAILURE_THRESHOLD = 5; -private readonly API_RETRY_ATTEMPTS = 3; -private isCircuitOpen = false; - -@Post("update-org") -public async cronjobUpdateOrg(@Request() request: RequestWithUser) { - // Idempotency check - if (this.isRunning) { - console.log("cronjobUpdateOrg: Job already running, skipping this execution"); - return new HttpSuccess({ - message: "Job already running", - skipped: true, - }); - } - - // Circuit breaker check - if (this.isCircuitOpen) { - console.warn("cronjobUpdateOrg: Circuit breaker is OPEN, skipping execution"); - return new HttpSuccess({ - message: "Circuit breaker is open", - skipped: true, - }); - } - - this.isRunning = true; - const startTime = Date.now(); - - try { - // Validate environment variables first - const apiUrl = process.env.API_URL; - const apiKey = process.env.API_KEY; - - if (!apiUrl) { - throw new Error("API_URL environment variable is not set"); - } - if (!apiKey) { - throw new Error("API_KEY environment variable is not set"); - } - - const windowStart = new Date(Date.now() - this.UPDATE_WINDOW_HOURS * 60 * 60 * 1000); - - console.log("cronjobUpdateOrg: Starting job", { - windowHours: this.UPDATE_WINDOW_HOURS, - windowStart: windowStart.toISOString(), - batchSize: this.BATCH_SIZE, - }); - - // ... existing database queries ... - - // Build payloads - const payloads = this.buildPayloads(posMasters, posMasterEmployee, posMasterEmployeeTemp); - - if (payloads.length === 0) { - console.log("cronjobUpdateOrg: No records to process"); - return new HttpSuccess({ - message: "No records to process", - processed: 0, - }); - } - - // Validate payload size before sending - if (payloads.length > 10000) { - console.warn(`cronjobUpdateOrg: Payload size ${payloads.length} exceeds 10000, splitting`); - } - - // Update profile's org structure in leave service with retry logic - console.log("cronjobUpdateOrg: Calling leave service API", { - payloadCount: payloads.length, - }); - - let apiSuccess = false; - let lastError: any; - - for (let attempt = 1; attempt <= this.API_RETRY_ATTEMPTS; attempt++) { - try { - await axios.put( - `${apiUrl}/leave-beginning/schedule/update-dna`, - payloads, - { - headers: { - "Content-Type": "application/json", - api_key: apiKey, - }, - timeout: 60000, // 60 second timeout (increased) - validateStatus: (status) => status < 500, // Retry on server errors only - } - ); - - apiSuccess = true; - this.apiFailureCount = 0; // Reset failure count on success - console.log(`cronjobUpdateOrg: API call succeeded on attempt ${attempt}`); - break; - - } catch (error: any) { - lastError = error; - console.error(`cronjobUpdateOrg: API call attempt ${attempt} failed:`, error.message); - - // Don't retry on client errors (4xx) - if (error.response?.status >= 400 && error.response?.status < 500) { - break; - } - - // Wait before retry with exponential backoff - if (attempt < this.API_RETRY_ATTEMPTS) { - const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } - - if (!apiSuccess) { - this.apiFailureCount++; - - // Open circuit breaker if threshold reached - if (this.apiFailureCount >= this.API_FAILURE_THRESHOLD) { - this.isCircuitOpen = true; - console.error("cronjobUpdateOrg: Circuit breaker OPENED due to repeated failures"); - - // Auto-close after 5 minutes - setTimeout(() => { - this.isCircuitOpen = false; - this.apiFailureCount = 0; - console.log("cronjobUpdateOrg: Circuit breaker CLOSED"); - }, 5 * 60 * 1000); - } - - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `ไม่สามารถเรียก Leave Service API ได้: ${lastError?.message || 'Unknown error'}` - ); - } - - // ... rest of the method ... - - } catch (error: any) { - const duration = Date.now() - startTime; - console.error("cronjobUpdateOrg: Job failed", { - duration: `${duration}ms`, - error: error.message, - stack: error.stack, - }); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `Internal server error: ${error?.message || 'Unknown error'}` - ); - } finally { - this.isRunning = false; - } -} -``` - ---- - -### 3. 🔴 CRITICAL - ScriptProfileOrgController.ts - Race Condition in Idempotency Check - -**File & Location:** [ScriptProfileOrgController.ts](src/controllers/ScriptProfileOrgController.ts:32-56) - Class properties and `cronjobUpdateOrg()` method - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -```typescript -private isRunning = false; - -@Post("update-org") -public async cronjobUpdateOrg(@Request() request: RequestWithUser) { - if (this.isRunning) { - console.log("cronjobUpdateOrg: Job already running, skipping this execution"); - return new HttpSuccess({ - message: "Job already running", - skipped: true, - }); - } - - this.isRunning = true; - // ... rest of the method - finally { - this.isRunning = false; - } -} -``` - -**ปัญหาที่พบ:** -1. **Race condition** - หากมี 2 requests มาพร้อมกัน `isRunning` อาจถูกตั้งค่าเป็น false โดยทั้ง 2 requests อ่านเป็น false -2. **Stuck state** - หากเกิด error ก่อนถึง finally block หรือ process crash ทิ้ง `isRunning` จะค้างที่ true ตลอดไป -3. **No timeout** - หาก job ทำงานนานเกินไป ไม่มีกลไก timeout -4. **Instance variable in containerized environment** - ในระบบ microservices ที่มีหลาย instances แต่ละ instance จะมี `isRunning` เป็นของตัวเอง ทำให้อาจมีการรันซ้ำกัน - -**Recommended Fix:** -```typescript -// Use Redis or database for distributed lock -import { createClient } from 'redis'; - -@Route("api/v1/org/script-profile-org") -@Tags("Keycloak Sync") -@Security("bearerAuth") -export class ScriptProfileOrgController extends Controller { - // ... existing repositories ... - - private readonly redisClient = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379', - socket: { - connectTimeout: 5000, - }, - }); - - private readonly LOCK_TIMEOUT = 10 * 60 * 1000; // 10 minutes - private readonly LOCK_KEY = 'cronjob:update-org:lock'; - - private async acquireLock(): Promise { - try { - await this.redisClient.connect(); - const result = await this.redisClient.set( - this.LOCK_KEY, - 'locked', - { - NX: true, // Only set if not exists - PX: this.LOCK_TIMEOUT, // Expire after timeout - } - ); - return result === 'OK'; - } catch (error) { - console.error('Failed to acquire lock:', error); - return false; - } finally { - await this.redisClient.quit(); - } - } - - private async releaseLock(): Promise { - try { - await this.redisClient.connect(); - await this.redisClient.del(this.LOCK_KEY); - } catch (error) { - console.error('Failed to release lock:', error); - } finally { - await this.redisClient.quit(); - } - } - - @Post("update-org") - public async cronjobUpdateOrg(@Request() request: RequestWithUser) { - // Try to acquire lock - const lockAcquired = await this.acquireLock(); - - if (!lockAcquired) { - console.log("cronjobUpdateOrg: Job already running or lock not acquired, skipping"); - return new HttpSuccess({ - message: "Job already running", - skipped: true, - }); - } - - const startTime = Date.now(); - - try { - // ... rest of the method ... - - const duration = Date.now() - startTime; - console.log("cronjobUpdateOrg: Job completed", { - duration: `${duration}ms`, - processed: payloads.length, - }); - - return new HttpSuccess({ - message: "Update org completed", - processed: payloads.length, - syncResults, - duration: `${duration}ms`, - }); - - } catch (error: any) { - const duration = Date.now() - startTime; - console.error("cronjobUpdateOrg: Job failed", { - duration: `${duration}ms`, - error: error.message, - stack: error.stack, - }); - - // Still release lock even on error - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `Internal server error: ${error?.message || 'Unknown error'}` - ); - } finally { - // Always release lock - await this.releaseLock(); - } - } -} -``` - ---- - -### 4. 🟡 HIGH - WorkflowController.ts - Missing Error Handling in Workflow Creation - -**File & Location:** [WorkflowController.ts](src/controllers/WorkflowController.ts:46-273) - `checkWorkflow()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -@Post("add-workflow") -public async checkWorkflow( - @Request() req: RequestWithUser, - @Body() body: { ... } -) { - const [userProfileOfficer, userProfileEmployee, metaWorkflow] = await Promise.all([ - this.profileRepo.findOne({...}), - this.profileEmployeeRepo.findOne({...}), - this.metaWorkflowRepo.findOne({...}), - ]); - - let profileType = "OFFICER"; - let profile: any = userProfileOfficer; - - if (!profile) { - profileType = "EMPLOYEE"; - profile = userProfileEmployee; - if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); - } - - if (!metaWorkflow) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบกระบวนการนี้ได้"); - - // ... สร้าง workflow ... - - const notificationReceivers = stateOperatorUsersToCreate - .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) - .map((user) => ({ - receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, - notiLink: notiLink, - })); - - // ส่ง notification แบบ fire-and-forget - new CallAPI() - .PostData(req, "/placement/noti/profiles", {...}) - .catch((error) => { - console.error("Error calling API:", error); - }); - - return new HttpSuccess(); -} -``` - -**ปัญหาที่พบ:** -1. **Partial error handling** - มี try-catch รอบ workflow creation แต่ไม่ครอบคลุมทั้งหมด -2. **Notification failure doesn't affect workflow** - หาก notification ล้ม จะไม่ทำให้ workflow ล้มด้วย ซึ่งอาจเป็นที่ต้องการ แต่ควรมีการ log ไว้ชัดเจน -3. **No cleanup on partial failure** - หากสร้าง workflow สำเร็จแต่สร้าง states ล้ม จะมีข้อมูล partial อยู่ใน database -4. **Missing transaction** - การสร้าง workflow มีหลายขั้นตอนแต่ไม่มี transaction - -**Recommended Fix:** -```typescript -@Post("add-workflow") -public async checkWorkflow( - @Request() req: RequestWithUser, - @Body() body: { ... } -) { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - // ขั้นที่ 1: ค้นหา profile และ metaWorkflow - const [userProfileOfficer, userProfileEmployee, metaWorkflow] = await Promise.all([ - queryRunner.manager.findOne(Profile, { - where: { keycloak: req.user.sub }, - select: ["id", "keycloak"], - }), - queryRunner.manager.findOne(ProfileEmployee, { - where: { keycloak: req.user.sub }, - select: ["id", "keycloak"], - }), - queryRunner.manager.findOne(MetaWorkflow, { - where: { - sysName: body.sysName, - posLevelName: body.posLevelName, - posTypeName: body.posTypeName, - }, - }), - ]); - - let profileType = "OFFICER"; - let profile: any = userProfileOfficer; - - if (!profile) { - profileType = "EMPLOYEE"; - profile = userProfileEmployee; - if (!profile) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); - } - } - - if (!metaWorkflow) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบกระบวนการนี้ได้"); - } - - const meta = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - // ขั้นที่ 2: สร้าง workflow และดึง metaState - const workflow = new Workflow(); - Object.assign(workflow, { - ...metaWorkflow, - id: undefined, - ...meta, - ...body, - profileType: profileType, - system: body.sysName, - }); - - const savedWorkflow = await queryRunner.manager.save(workflow); - - const metaStates = await queryRunner.manager.find(MetaState, { - where: { metaWorkflowId: metaWorkflow.id }, - order: { order: "ASC" }, - }); - - // ขั้นที่ 3: สร้าง states - const statesToCreate = metaStates.map((item) => { - const state = new State(); - Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta }); - return state; - }); - - const savedStates = await queryRunner.manager.save(statesToCreate); - - // ขั้นที่ 4: อัปเดต workflow.stateId - const firstState = savedStates.find((state) => state.order === 1); - if (firstState) { - savedWorkflow.stateId = firstState.id; - await queryRunner.manager.save(savedWorkflow); - } - - // ขั้นที่ 5: ดึงและสร้าง stateOperators - const metaStateIds = metaStates.map((item) => item.id); - const allMetaStateOperators = await queryRunner.manager.find(MetaStateOperator, { - where: { metaStateId: In(metaStateIds) }, - }); - - const stateOperatorsToCreate: StateOperator[] = []; - - allMetaStateOperators.forEach((metaStateOp) => { - const correspondingState = savedStates.find( - (state) => - metaStates.find((metaState) => metaState.id === metaStateOp.metaStateId)?.order === - state.order, - ); - - if (body.isDeputy) { - if (body.sysName == "SYS_TRANSFER_REQ") { - if (metaStateOp.operator == "PersonnelOfficer" && correspondingState?.order == 1) { - return; - } else if ( - metaStateOp.operator == "Officer" && - [1, 2].includes(correspondingState?.order as number) - ) { - metaStateOp.operator = "PersonnelOfficer"; - } - } - if ( - metaStateOp.operator == "Officer" && - ["REGISTRY_PROFILE", "REGISTRY_PROFILE_EMP", "REGISTRY_IDP"].includes(body.sysName) - ) { - metaStateOp.operator = "PersonnelOfficer"; - } - } - - if (correspondingState) { - const stateOperator = new StateOperator(); - Object.assign(stateOperator, { - ...metaStateOp, - id: undefined, - stateId: correspondingState.id, - ...meta, - }); - stateOperatorsToCreate.push(stateOperator); - } - }); - - await queryRunner.manager.save(stateOperatorsToCreate); - - // ขั้นที่ 6: สร้าง StateOperatorUsers - const stateOperatorUsersToCreate: StateOperatorUser[] = []; - let orderNum = 1; - - if (profile) { - const ownerStateOperatorUser = new StateOperatorUser(); - Object.assign(ownerStateOperatorUser, { - profileId: profileType === "OFFICER" ? profile.id : null, - profileEmployeeId: profileType !== "OFFICER" ? profile.id : null, - profileType: profileType, - operator: "Owner", - order: orderNum, - workflowId: savedWorkflow.id, - ...meta, - }); - stateOperatorUsersToCreate.push(ownerStateOperatorUser); - } - - const profileOfficers = await queryRunner.manager.find(PosMaster, { - where: { - posMasterAssigns: { assignId: body.sysName }, - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - current_holderId: Not(IsNull()), - ...(body.orgRootId && { orgRootId: body.orgRootId }), - }, - relations: ["orgChild1"], - }); - - profileOfficers.forEach((item) => { - if (item.current_holderId) { - orderNum += 1; - const isPersonnelOfficer = item.orgChild1?.isOfficer === true; - - const officerStateOperatorUser = new StateOperatorUser(); - Object.assign(officerStateOperatorUser, { - profileId: item.current_holderId, - operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer", - profileType: "OFFICER", - order: orderNum, - workflowId: savedWorkflow.id, - ...meta, - }); - stateOperatorUsersToCreate.push(officerStateOperatorUser); - } - }); - - await queryRunner.manager.save(stateOperatorUsersToCreate); - - // Commit transaction before sending notification - await queryRunner.commitTransaction(); - - // ขั้นที่ 7: ส่ง notification (outside transaction) - const firstStateOperators = stateOperatorsToCreate.filter((so) => - savedStates.find((state) => state.id === so.stateId && state.order === 1), - ); - - let notiLink = ""; - if (body.sysName === "REGISTRY_PROFILE") { - notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`; - } else if (body.sysName === "REGISTRY_PROFILE_EMP") { - notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`; - } else if (body.sysName === "REGISTRY_IDP") { - notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`; - } - - const notificationReceivers = stateOperatorUsersToCreate - .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) - .map((user) => ({ - receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, - notiLink: notiLink, - })); - - // ส่ง notification แบบ fire-and-forget แต่ log ไว้ชัดเจน - new CallAPI() - .PostData(req, "/placement/noti/profiles", { - subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, - body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, - receiverUserIds: notificationReceivers, - payload: "", - isSendMail: true, - isSendInbox: true, - isSendNotification: true, - }) - .then(() => { - console.log(`[Workflow] Notification sent successfully for workflow ${savedWorkflow.id}`); - }) - .catch((error) => { - console.error(`[Workflow] Failed to send notification for workflow ${savedWorkflow.id}:`, error); - // Log แต่ไม่ throw เพราะ workflow สร้างสำเร็จแล้ว - }); - - return new HttpSuccess(); - - } catch (error: any) { - await queryRunner.rollbackTransaction(); - console.error('[Workflow] Failed to create workflow:', error); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `ไม่สามารถสร้าง workflow ได้: ${error?.message || 'Unknown error'}` - ); - } finally { - await queryRunner.release(); - } -} -``` - ---- - -### 5. 🟡 MEDIUM - ProfileSalaryTempController.ts - SQL Injection Risk - -**File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts:173-225) - `listProfile()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -.andWhere( - new Brackets((qb) => { - qb.orWhere( - searchKeyword != null && searchKeyword != "" - ? `profile.citizenId like '%${searchKeyword}%'` - : "1=1", - ) - // ... หลาย orWhere โดยใส่ค่าโดยตรง - }), -) -``` - -**ปัญหาที่พบ:** -1. **SQL Injection vulnerability** - การใส่ `searchKeyword` โดยตรงเข้าไปใน query string อาจทำให้เกิด SQL injection -2. **Query syntax error** - หาก `searchKeyword` มีตัวอักษรพิเศษเช่น single quote (`'`) จะทำให้ query error -3. **No input sanitization** - ไม่มีการ sanitize ข้อมูลก่อนใช้ใน query - -**Recommended Fix:** -```typescript -.andWhere( - new Brackets((qb) => { - const keywordParam = `%${searchKeyword}%`; - - if (searchKeyword != null && searchKeyword != "") { - qb.orWhere("profile.citizenId LIKE :keyword", { keyword: keywordParam }) - .orWhere("profile.position LIKE :keyword", { keyword: keywordParam }) - .orWhere( - "CONCAT(profile.prefix, profile.firstName, ' ', profile.lastName) LIKE :keyword", - { keyword: keywordParam } - ) - .orWhere("posType.posTypeName LIKE :keyword", { keyword: keywordParam }) - .orWhere("posLevel.posLevelName LIKE :keyword", { keyword: keywordParam }) - .orWhere("orgRoot.orgRootName LIKE :keyword", { keyword: keywordParam }) - // ... ใช้ parameterized query ทุกครั้ง - } else { - qb.where("1=1"); - } - }), -) -``` - ---- - -### 6. 🟡 MEDIUM - WorkflowController.ts - Invalid Switch Case - -**File & Location:** [WorkflowController.ts](src/controllers/WorkflowController.ts:311-337) - `checkIsCan()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -switch (body.action.trim().toLocaleUpperCase()) { - case "VIEW": - isCan = operator.canView; - case "UPDATE": - isCan = operator.canUpdate; - case "DELETE": - isCan = operator.canDelete; - case "CANCEL": - isCan = operator.canCancel; - case "OPERATE": - isCan = operator.canOperate; - case "CHANGESTATE": - isCan = operator.canChangeState; - case "COMMENT": - isCan = operator.canComment; - case "SIGN": - isCan = operator.canSign; - default: - isCan = false; -} -``` - -**ปัญหาที่พบ:** -1. **Missing break statements** - ไม่มี `break` หลังแต่ละ case ทำให้ fall-through ไป case ถัดไปเสมอ -2. **Logic error** - เช่นถ้า action เป็น "VIEW" จะเช็ค canView แล้ว fall-through ไปเช็ค canUpdate, canDelete, ... และสุดท้ายจะเป็นค่าจาก default case - -**Recommended Fix:** -```typescript -switch (body.action.trim().toLocaleUpperCase()) { - case "VIEW": - isCan = operator.canView; - break; - case "UPDATE": - isCan = operator.canUpdate; - break; - case "DELETE": - isCan = operator.canDelete; - break; - case "CANCEL": - isCan = operator.canCancel; - break; - case "OPERATE": - isCan = operator.canOperate; - break; - case "CHANGESTATE": - isCan = operator.canChangeState; - break; - case "COMMENT": - isCan = operator.canComment; - break; - case "SIGN": - isCan = operator.canSign; - break; - default: - isCan = false; - break; -} -``` - ---- - -### 7. 🟡 MEDIUM - ProfileTrainingController.ts - Unhandled Promise Rejection - -**File & Location:** [ProfileTrainingController.ts](src/controllers/ProfileTrainingController.ts:158-163) - `editTraining()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -await Promise.all([ - this.trainingRepo.save(record, { data: req }), - setLogDataDiff(req, { before, after: record }), - this.trainingHistoryRepo.save(history, { data: req }), -]); -``` - -**ปัญหาที่พบ:** -1. **Unhandled Promise rejection** - หาก operation ใดๆ ใน Promise.all ล้ม จะเกิด unhandled rejection -2. **Partial data inconsistency** - หาก `save(record)` สำเร็จ แต่ `save(history)` ล้ม จะมีข้อมูลไม่สอดคล้องกัน -3. **No try-catch** - ไม่มีการ handle error เลย - -**Recommended Fix:** -```typescript -@Patch("{trainingId}") -public async editTraining( - @Request() req: RequestWithUser, - @Body() body: UpdateProfileTraining, - @Path() trainingId: string, -) { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const record = await queryRunner.manager.findOne(ProfileTraining, { - where: { id: trainingId } - }); - - 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 ProfileTrainingHistory(); - - Object.assign(record, body); - Object.assign(history, { ...record, id: undefined }); - - history.profileTrainingId = trainingId; - 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(); - - // Save within transaction - await queryRunner.manager.save(record); - - // Log outside transaction but handle error gracefully - try { - setLogDataDiff(req, { before, after: record }); - } catch (logError) { - console.error('[ProfileTraining] Failed to log data diff:', logError); - // Continue anyway - log failure shouldn't break the operation - } - - await queryRunner.manager.save(history); - await queryRunner.commitTransaction(); - - return new HttpSuccess(); - - } catch (error: any) { - await queryRunner.rollbackTransaction(); - - if (error instanceof HttpError) { - throw error; - } - - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `ไม่สามารถแก้ไขข้อมูลได้: ${error?.message || 'Unknown error'}` - ); - } finally { - await queryRunner.release(); - } -} -``` - ---- - -### 8. 🟢 LOW - ProfileTrainingController.ts - Missing Validation - -**File & Location:** [ProfileTrainingController.ts](src/controllers/ProfileTrainingController.ts:233-260) - `deleteAllTraining()` method - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -```typescript -@Post("delete-all") -public async deleteAllTraining( - @Body() reqBody: { developmentId: string }, - @Request() req: RequestWithUser -) { - 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 - }); - - return new HttpSuccess(); -} -``` - -**ปัญหาที่พบ:** -1. **No input validation** - ไม่ validate `developmentId` ว่ามีค่าหรือไม่ -2. **No try-catch** - ไม่มี error handling เลย -3. **No transaction** - การลบข้อมูลหลายตารางไม่อยู่ใน transaction อาจเกิด partial delete - -**Recommended Fix:** -```typescript -@Post("delete-all") -public async deleteAllTraining( - @Body() reqBody: { developmentId: string }, - @Request() req: RequestWithUser -) { - // Validate input - if (!reqBody.developmentId || reqBody.developmentId.trim() === "") { - throw new HttpError(HttpStatus.BAD_REQUEST, "developmentId ต้องไม่ว่างเปล่า"); - } - - 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({ - message: "ลบข้อมูลเสร็จสิ้น", - deletedTrainings: trainings.length - }); - - } catch (error: any) { - await queryRunner.rollbackTransaction(); - - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `ไม่สามารถลบข้อมูลได้: ${error?.message || 'Unknown error'}` - ); - } finally { - await queryRunner.release(); - } -} -``` - ---- - -## สรุปสถิติ - -| ระดับความรุนแรง | จำนวน | รายการ | -|---|---|---| -| 🔴 CRITICAL | 3 | 1, 2, 3 | -| 🟡 HIGH | 1 | 4 | -| 🟡 MEDIUM | 2 | 5, 6 | -| 🟢 LOW | 1 | 7 | - ---- - -## คำแนะนำการจัดลำดับการแก้ไข - -### แก้ไขทันที (P0 - Critical) -1. **ProfileSalaryTempController.ts** - `changeSortEditGenAll()` method - - เพิ่ม pagination - - เพิ่ม transaction management - - เพิ่ม error handling per iteration - -2. **ScriptProfileOrgController.ts** - External API calls - - เพิ่ม retry logic - - เพิ่ม circuit breaker - - แก้ race condition ใน idempotency check - -3. **ScriptProfileOrgController.ts** - Distributed locking - - ใช้ Redis หรือ database lock แทน instance variable - - เพิ่ม auto-release mechanism - -### แก้ไขเร็วๆ นี้ (P1 - High) -4. **WorkflowController.ts** - Transaction management - - เพิ่ม transaction สำหรับ workflow creation - - เพิ่ม cleanup บน partial failure - -### แก้ไขในภายหลัง (P2 - Medium) -5. **ProfileSalaryTempController.ts** - SQL Injection prevention - - ใช้ parameterized query - -6. **WorkflowController.ts** - Fix switch case - - เพิ่ม break statements - -### แก้ไขเมื่อว่าง (P3 - Low) -7. **ProfileTrainingController.ts** - Input validation - - เพิ่ม validation และ error handling - ---- - -## ข้อเสนอแนะเพิ่มเติม - -1. **Redis/Distributed Lock** - สำหรับ cronjobs ใน containerized environment -2. **Circuit Breaker Pattern** - สำหรับ external API calls -3. **Retry with Exponential Backoff** - สำหรับ operations ที่อาจล้มชั่วคราว -4. **Structured Logging** - เพิ่ม logging ที่มีโครงสร้างเพื่อ debugging และ monitoring -5. **Health Check Endpoints** - สำหรับตรวจสอบสถานะของ service และ dependencies -6. **Graceful Shutdown** - ให้แน่ใจว่า long-running operations สามารถ handle shutdown ได้ - ---- - -## ไฟล์รายงานที่เกี่ยวข้อง - -- [Batch 1-10 Analysis](../reports/) - รายงานการตรวจสอบ Controllers ชุดก่อนหน้า -- [Security Audit Report](../reports/security-audit.md) - รายงานการตรวจสอบด้านความปลอดภัย diff --git a/reports/batch-12-controllers-111-120-analysis.md b/reports/batch-12-controllers-111-120-analysis.md deleted file mode 100644 index 9fe66496..00000000 --- a/reports/batch-12-controllers-111-120-analysis.md +++ /dev/null @@ -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 จุด - ปัญหาความปลอดภัยและความสอดคล้องของระบบ diff --git a/reports/batch-13-controllers-121-130-analysis.md b/reports/batch-13-controllers-121-130-analysis.md deleted file mode 100644 index a889a7f6..00000000 --- a/reports/batch-13-controllers-121-130-analysis.md +++ /dev/null @@ -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 diff --git a/reports/batch-14-controllers-131-140-analysis.md b/reports/batch-14-controllers-131-140-analysis.md deleted file mode 100644 index a76e0e0b..00000000 --- a/reports/batch-14-controllers-131-140-analysis.md +++ /dev/null @@ -1,1422 +0,0 @@ -# Batch 14 Controllers Analysis (Controllers 131-140) - -## Controllers in this batch: -1. RankController -2. RelationshipController -3. ReligionController -4. ReportController (partial - file too large, analyzed first 100 lines) -5. ScriptProfileOrgController -6. SocketController -7. SubDistrictController -8. UserController -9. ViewWorkFlowController -10. WorkflowController - ---- - -## Critical Issues Found - -### 1. **UserController** - Multiple Unhandled forEach Async Operations - -**File & Location:** [UserController.ts](src/controllers/UserController.ts) - Methods: `createUserImport()`, `addroleStaffToUser()`, `addroleStaffToUserEmp()`, `changeUserPasswordAll()` - -**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle - -**Root Cause:** -Multiple critical methods use `for await` loops and `forEach()` with async Keycloak API operations without proper error handling. When Keycloak operations fail, the errors can crash the Node.js process. - -**Affected Code Locations:** -- Line 977-1032: `for await` loop in `createUserImport()` - No error handling for individual user creation failures -- Line 1133-1148: `for await` loop in `changeUserPasswordAll()` - Errors are silently ignored but not properly handled -- Line 1169-1227: `for await` loop in `addroleStaffToUser()` - Keycloak operations without error handling -- Line 1249-1307: `for await` loop in `addroleStaffToUserEmp()` - Keycloak operations without error handling -- Line 1066-1118: `Promise.all()` in `createUserImportEmp()` - Limited error handling - -**Code Examples:** - -```typescript -// Line 977-1032 - DANGEROUS: for await without error handling -for await (const _item of profiles) { - let password = _item.citizenId; - if (_item.birthDate != null) { - const _date = new Date(_item.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - const checkUser = await getUserByUsername(_item.citizenId); - let userId: any = ""; - if (checkUser.length == 0) { - userId = await createUser(_item.citizenId, password, { - firstName: _item.firstName, - lastName: _item.lastName, - }); - if (typeof userId !== "string") { - throw new Error(userId.errorMessage); // This can crash the entire process - } - } else { - userId = checkUser[0].id; - } - - const list = await getRoles(); - if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); - const result = await addUserRoles( - userId, - list.filter((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212"), - ); - - if (!result) { - throw new Error("Failed. Cannot set user's role."); // This can crash the entire process - } - // ... more operations without error handling -} -``` - -```typescript -// Line 1066-1118 - Promise.all with some error handling but not comprehensive -await Promise.all( - batch.map(async (_item) => { - // ... operations - try { - const checkUser = await getUserByUsername(_item.citizenId); - // ... more operations - } catch (error) { - console.error(`Error processing ${_item.citizenId}:`, error); - } - }), -); -``` - -**Recommended Fix:** - -```typescript -// For createUserImport() - Add comprehensive error handling -@Post("user/create") -@Security("bearerAuth", ["system", "admin"]) -async createUserImport( - @Request() request: { user: { sub: string; preferred_username: string } }, -) { - const profiles = await this.profileRepo.find({ - where: { - keycloak: IsNull(), - }, - relations: ["roleKeycloaks"], - }); - - const results = { - total: profiles.length, - success: 0, - failed: 0, - errors: [] as Array<{ citizenId: string; error: string }>, - }; - - // Cache roles list to avoid repeated API calls - let rolesList: any[] = []; - try { - rolesList = await getRoles(); - if (!Array.isArray(rolesList)) { - throw new Error("Failed. Cannot get role(s) data from the server."); - } - } catch (error) { - console.error('Failed to fetch roles:', error); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to fetch roles from Keycloak' - ); - } - - const defaultRole = rolesList.find((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212"); - if (!defaultRole) { - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Default role not found in Keycloak' - ); - } - - for await (const _item of profiles) { - try { - let password = _item.citizenId; - if (_item.birthDate != null) { - const _date = new Date(_item.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - - const checkUser = await getUserByUsername(_item.citizenId); - let userId: string = ""; - - if (checkUser.length == 0) { - const createdUser = await createUser(_item.citizenId, password, { - firstName: _item.firstName, - lastName: _item.lastName, - }); - - if (typeof createdUser !== "string") { - throw new Error(createdUser.errorMessage || 'Failed to create user'); - } - userId = createdUser; - } else { - userId = checkUser[0].id; - } - - const result = await addUserRoles(userId, [defaultRole]); - - if (!result) { - throw new Error("Failed. Cannot set user's role."); - } - - if (typeof userId === "string") { - _item.keycloak = userId; - } - - const roleKeycloak = await this.roleKeycloakRepo.find({ - where: { id: "8a1a0dc9-304c-4e5b-a90a-65f841048212" }, - }); - - if (_item) { - _item.roleKeycloaks = Array.from(new Set([..._item.roleKeycloaks, ...roleKeycloak])); - await this.profileRepo.save(_item); - } - - results.success++; - } catch (error: any) { - results.failed++; - results.errors.push({ - citizenId: _item.citizenId, - error: error.message || 'Unknown error', - }); - console.error(`Error processing user ${_item.citizenId}:`, error); - } - } - - return new HttpSuccess({ - message: 'User import completed', - ...results, - }); -} - -// For addroleStaffToUser() - Add error handling with detailed logging -@Post("add-role-staff/user/{child1Id}") -@Security("bearerAuth", ["system", "admin"]) -async addroleStaffToUser( - @Path() child1Id: string, - @Request() request: { user: { sub: string; preferred_username: string } }, -) { - const profiles = await this.profileRepo.find({ - where: { - keycloak: Not(IsNull()), - current_holders: { - orgChild1Id: child1Id, - }, - }, - relations: ["roleKeycloaks"], - }); - - const results = { - total: profiles.length, - success: 0, - failed: 0, - errors: [] as Array<{ citizenId: string; error: string }>, - }; - - // Cache roles - let rolesList: any[] = []; - try { - rolesList = await getRoles(); - if (!Array.isArray(rolesList)) { - throw new Error("Failed. Cannot get role(s) data from the server."); - } - } catch (error) { - console.error('Failed to fetch roles:', error); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to fetch roles from Keycloak' - ); - } - - const userRole = rolesList.find((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212"); - const staffRole = rolesList.find((v) => v.id == "f1fff8db-0795-47c1-9952-f3c18d5b6172"); - - if (!userRole || !staffRole) { - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Required roles not found in Keycloak' - ); - } - - for await (const _item of profiles) { - try { - let password = _item.citizenId; - if (_item.birthDate != null) { - const _date = new Date(_item.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - - const checkUser = await getUserByUsername(_item.citizenId); - let userId: string = ""; - - if (checkUser.length == 0) { - const createdUser = await createUser(_item.citizenId, password, { - firstName: _item.firstName, - lastName: _item.lastName, - }); - - if (typeof createdUser !== "string") { - throw new Error(createdUser.errorMessage || 'Failed to create user'); - } - userId = createdUser; - } else { - userId = checkUser[0].id; - } - - // Add both roles - await Promise.all([ - addUserRoles(userId, [userRole]), - addUserRoles(userId, [staffRole]), - ]); - - if (typeof userId === "string") { - _item.keycloak = userId; - } - - const roleKeycloakUser = await this.roleKeycloakRepo.find({ - where: { id: "8a1a0dc9-304c-4e5b-a90a-65f841048212" }, - }); - const roleKeycloakStaff = await this.roleKeycloakRepo.find({ - where: { id: "f1fff8db-0795-47c1-9952-f3c18d5b6172" }, - }); - - if (_item) { - _item.roleKeycloaks = Array.from(new Set([...roleKeycloakUser, ...roleKeycloakStaff])); - await this.profileRepo.save(_item); - } - - results.success++; - } catch (error: any) { - results.failed++; - results.errors.push({ - citizenId: _item.citizenId, - error: error.message || 'Unknown error', - }); - console.error(`Error processing user ${_item.citizenId}:`, error); - } - } - - return new HttpSuccess({ - message: 'Role assignment completed', - ...results, - }); -} - -// For changeUserPasswordAll() - Add proper error handling -@Post("user/change-password-all") -async changeUserPasswordAll( - @Request() request: { user: { sub: string; preferred_username: string } }, -) { - const profiles = await this.profileRepo.find({ - where: { - keycloak: Not(IsNull()), - }, - }); - - const results = { - total: profiles.length, - success: 0, - failed: 0, - errors: [] as Array<{ citizenId: string; error: string }>, - }; - - for await (const _item of profiles) { - try { - let password = _item.citizenId; - if (_item.birthDate != null) { - const gregorianYear = _item.birthDate.getFullYear() + 543; - - const formattedDate = - _item.birthDate.toISOString().slice(8, 10) + - _item.birthDate.toISOString().slice(5, 7) + - gregorianYear; - password = formattedDate; - } - - const result = await changeUserPassword(_item.keycloak, password); - if (!result) { - throw new Error('Failed to change password'); - } - - results.success++; - } catch (error: any) { - results.failed++; - results.errors.push({ - citizenId: _item.citizenId, - error: error.message || 'Unknown error', - }); - console.error(`Error changing password for ${_item.citizenId}:`, error); - } - } - - return new HttpSuccess({ - message: 'Password change completed', - ...results, - }); -} -``` - ---- - -### 2. **WorkflowController** - Multiple Database Operations Without Transactions - -**File & Location:** [WorkflowController.ts](src/controllers/WorkflowController.ts) - Method: `checkWorkflow()` - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -Complex multi-step workflow creation process without transactional integrity. If intermediate operations fail, the database can be left in an inconsistent state with partial data. - -**Affected Code Locations:** -- Line 46-273: `checkWorkflow()` - Multiple sequential database operations without transaction -- Line 143-180: `forEach()` with state operator creation - No error handling for individual operations -- Line 214-230: `forEach()` for officer state operator users - No error handling -- Line 258-270: Fire-and-forget API call with only console error logging - -**Code Examples:** - -```typescript -// Line 111-133 - Multiple database operations without transaction -const [savedWorkflow, metaStates] = await Promise.all([ - this.workflowRepo.save(workflow), - this.metaStateRepo.find({ - where: { metaWorkflowId: metaWorkflow.id }, - order: { order: "ASC" }, - }), -]); - -// ขั้นที่ 3: สร้าง states ทั้งหมดในครั้งเดียว -const statesToCreate = metaStates.map((item) => { - const state = new State(); - Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta }); - return state; -}); - -const savedStates = await this.stateRepo.save(statesToCreate); - -// ขั้นที่ 4: อัปเดต workflow.stateId กับ state แรก -const firstState = savedStates.find((state) => state.order === 1); -if (firstState) { - savedWorkflow.stateId = firstState.id; - await this.workflowRepo.save(savedWorkflow); -} -``` - -```typescript -// Line 214-230 - forEach without error handling -profileOfficers.forEach((item) => { - if (item.current_holderId) { - orderNum += 1; - const isPersonnelOfficer = item.orgChild1?.isOfficer === true; - - const officerStateOperatorUser = new StateOperatorUser(); - Object.assign(officerStateOperatorUser, { - profileId: item.current_holderId, - operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer", - profileType: "OFFICER", - order: orderNum, - workflowId: savedWorkflow.id, - ...meta, - }); - stateOperatorUsersToCreate.push(officerStateOperatorUser); - } -}); -``` - -```typescript -// Line 258-270 - Fire-and-forget API call -new CallAPI() - .PostData(req, "/placement/noti/profiles", { - subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, - body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, - receiverUserIds: notificationReceivers, - payload: "", - isSendMail: true, - isSendInbox: true, - isSendNotification: true, - }) - .catch((error) => { - console.error("Error calling API:", error); - }); -``` - -**Recommended Fix:** - -```typescript -@Post("add-workflow") -public async checkWorkflow( - @Request() req: RequestWithUser, - @Body() - body: { - refId: string; - sysName: string; - posLevelName: string; - posTypeName: string; - fullName?: string | null; - isDeputy?: boolean | null; - orgRootId?: string | null; - }, -) { - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - // ขั้นที่ 1: ทำการค้นหา profile และ metaWorkflow แบบ parallel - const [userProfileOfficer, userProfileEmployee, metaWorkflow] = await Promise.all([ - queryRunner.manager.findOne(Profile, { - where: { keycloak: req.user.sub }, - select: ["id", "keycloak"], - }), - queryRunner.manager.findOne(ProfileEmployee, { - where: { keycloak: req.user.sub }, - select: ["id", "keycloak"], - }), - queryRunner.manager.findOne(MetaWorkflow, { - where: { - sysName: body.sysName, - posLevelName: body.posLevelName, - posTypeName: body.posTypeName, - }, - }), - ]); - - // กำหนด profile type และ profile - let profileType = "OFFICER"; - let profile: any = userProfileOfficer; - - if (!profile) { - profileType = "EMPLOYEE"; - profile = userProfileEmployee; - if (!profile) { - await queryRunner.rollbackTransaction(); - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); - } - } - - if (!metaWorkflow) { - await queryRunner.rollbackTransaction(); - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบกระบวนการนี้ได้"); - } - - const meta = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - // ขั้นที่ 2: สร้าง workflow และดึง metaState แบบ parallel - const workflow = new Workflow(); - Object.assign(workflow, { - ...metaWorkflow, - id: undefined, - ...meta, - ...body, - profileType: profileType, - system: body.sysName, - }); - - const savedWorkflow = await queryRunner.manager.save(workflow); - - const metaStates = await queryRunner.manager.find(MetaState, { - where: { metaWorkflowId: metaWorkflow.id }, - order: { order: "ASC" }, - }); - - // ขั้นที่ 3: สร้าง states ทั้งหมดในครั้งเดียว - const statesToCreate = metaStates.map((item) => { - const state = new State(); - Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta }); - return state; - }); - - const savedStates = await queryRunner.manager.save(statesToCreate); - - // ขั้นที่ 4: อัปเดต workflow.stateId กับ state แรก - const firstState = savedStates.find((state) => state.order === 1); - if (firstState) { - savedWorkflow.stateId = firstState.id; - await queryRunner.manager.save(savedWorkflow); - } - - // ขั้นที่ 5: ดึง metaStateOperators ทั้งหมดและสร้าง stateOperators - const metaStateIds = metaStates.map((item) => item.id); - const allMetaStateOperators = await queryRunner.manager.find(MetaStateOperator, { - where: { metaStateId: In(metaStateIds) }, - }); - - // สร้าง stateOperators ทั้งหมดในครั้งเดียว - const stateOperatorsToCreate: StateOperator[] = []; - allMetaStateOperators.forEach((metaStateOp) => { - const correspondingState = savedStates.find( - (state) => - metaStates.find((metaState) => metaState.id === metaStateOp.metaStateId)?.order === - state.order, - ); - if (body.isDeputy) { - // Task #2207 กรณีคนขอโอนอยู่ในสำนักปลัดกรุงเทพมหานคร - if (body.sysName == "SYS_TRANSFER_REQ") { - if (metaStateOp.operator == "PersonnelOfficer" && correspondingState?.order == 1) { - return; - } else if ( - metaStateOp.operator == "Officer" && - [1, 2].includes(correspondingState?.order as number) - ) { - metaStateOp.operator = "PersonnelOfficer"; - } - } - // Task #2208 กรณีขอแก้ไขข้อมูลทะเบียนประวัติ และ IDP และคนขออยู่ในสำนักปลัดกรุงเทพมหานคร - if ( - metaStateOp.operator == "Officer" && - ["REGISTRY_PROFILE", "REGISTRY_PROFILE_EMP", "REGISTRY_IDP"].includes(body.sysName) - ) { - metaStateOp.operator = "PersonnelOfficer"; - } - } - if (correspondingState) { - const stateOperator = new StateOperator(); - Object.assign(stateOperator, { - ...metaStateOp, - id: undefined, - stateId: correspondingState.id, - ...meta, - }); - stateOperatorsToCreate.push(stateOperator); - } - }); - - await queryRunner.manager.save(stateOperatorsToCreate); - - // ขั้นที่ 6: สร้าง StateOperatorUsers แบบ bulk - const stateOperatorUsersToCreate: StateOperatorUser[] = []; - let orderNum = 1; - - // เพิ่ม Owner ก่อน - if (profile) { - const ownerStateOperatorUser = new StateOperatorUser(); - Object.assign(ownerStateOperatorUser, { - profileId: profileType === "OFFICER" ? profile.id : null, - profileEmployeeId: profileType !== "OFFICER" ? profile.id : null, - profileType: profileType, - operator: "Owner", - order: orderNum, - workflowId: savedWorkflow.id, - ...meta, - }); - stateOperatorUsersToCreate.push(ownerStateOperatorUser); - } - - // ดึงข้อมูล profileOfficers และสร้าง StateOperatorUsers - const profileOfficers = await queryRunner.manager.find(PosMaster, { - where: { - posMasterAssigns: { assignId: body.sysName }, - orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, - current_holderId: Not(IsNull()), - ...(body.orgRootId && { orgRootId: body.orgRootId }), - }, - relations: ["orgChild1"], - }); - - // สร้าง StateOperatorUsers สำหรับ officers - with error handling - for (const item of profileOfficers) { - try { - if (item.current_holderId) { - orderNum += 1; - const isPersonnelOfficer = item.orgChild1?.isOfficer === true; - - const officerStateOperatorUser = new StateOperatorUser(); - Object.assign(officerStateOperatorUser, { - profileId: item.current_holderId, - operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer", - profileType: "OFFICER", - order: orderNum, - workflowId: savedWorkflow.id, - ...meta, - }); - stateOperatorUsersToCreate.push(officerStateOperatorUser); - } - } catch (error) { - console.error(`Error processing officer ${item.current_holderId}:`, error); - // Continue with next officer - } - } - - // บันทึก StateOperatorUsers ทั้งหมดในครั้งเดียว - await queryRunner.manager.save(stateOperatorUsersToCreate); - - // Commit transaction - await queryRunner.commitTransaction(); - - // ขั้นที่ 7: ส่ง notification (fire-and-forget after transaction commits) - const firstStateOperators = stateOperatorsToCreate.filter((so) => - savedStates.find((state) => state.id === so.stateId && state.order === 1), - ); - - let notiLink = ""; - if (body.sysName === "REGISTRY_PROFILE") { - notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`; - } else if (body.sysName === "REGISTRY_PROFILE_EMP") { - notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`; - } else if (body.sysName === "REGISTRY_IDP") { - notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`; - } - - const notificationReceivers = stateOperatorUsersToCreate - .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) - .map((user) => ({ - receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, - notiLink: notiLink, - })); - - // Send notification asynchronously with proper error handling - new CallAPI() - .PostData(req, "/placement/noti/profiles", { - subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, - body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, - receiverUserIds: notificationReceivers, - payload: "", - isSendMail: true, - isSendInbox: true, - isSendNotification: true, - }) - .catch((error) => { - console.error("Error calling notification API:", { - workflowId: savedWorkflow.id, - error: error instanceof Error ? error.message : error, - }); - }); - - return new HttpSuccess(); - } catch (error) { - await queryRunner.rollbackTransaction(); - console.error('Error in checkWorkflow:', { - refId: body.refId, - sysName: body.sysName, - error: error instanceof Error ? error.message : error, - stack: error instanceof Error ? error.stack : undefined, - }); - - if (error instanceof HttpError) { - throw error; - } - - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to create workflow' - ); - } finally { - await queryRunner.release(); - } -} -``` - ---- - -### 3. **ScriptProfileOrgController** - Missing Error Handling for External API Calls - -**File & Location:** [ScriptProfileOrgController.ts](src/controllers/ScriptProfileOrgController.ts) - Method: `cronjobUpdateOrg()` - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -While this controller has good error handling structure, the external API call to the leave service and Keycloak sync operations need better error handling for individual batch failures. - -**Affected Code Locations:** -- Line 184-190: External API call without detailed error handling -- Line 228-250: Batch processing with limited error context -- Line 71-159: Complex database queries without error handling - -**Code Examples:** - -```typescript -// Line 184-190 - External API call needs better error handling -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, -}); -``` - -```typescript -// Line 228-250 - Batch processing could use more error context -try { - const batchResult: any = await keycloakSyncController.syncByProfileIds({ - profileIds: batch, - profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", - }); - - const resultData = (batchResult as any)?.data || batchResult; - typeResult.success += resultData.success || 0; - typeResult.failed += resultData.failed || 0; - - console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { - success: resultData.success || 0, - failed: resultData.failed || 0, - }); -} catch (error: any) { - console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { - error: error.message, - batchSize: batch.length, - }); - typeResult.failed += batch.length; -} -``` - -**Recommended Fix:** - -```typescript -// Improve external API call error handling -try { - const response = 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, - } - ); - - console.log("cronjobUpdateOrg: Leave service API call successful", { - status: response.status, - payloadCount: payloads.length, - }); -} catch (error: any) { - console.error("cronjobUpdateOrg: Leave service API call failed", { - error: error.message, - response: error.response?.data, - status: error.response?.status, - payloadCount: payloads.length, - }); - - // Don't fail completely - log and continue - // Optionally: implement retry logic or circuit breaker -} - -// Improve batch processing error handling -for (let i = 0; i < batches.length; i++) { - const batch = batches[i]; - console.log( - `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`, - { - batchSize: batch.length, - batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min( - (i + 1) * this.BATCH_SIZE, - profileIds.length, - )}`, - }, - ); - - try { - const batchResult: any = await keycloakSyncController.syncByProfileIds({ - profileIds: batch, - profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", - }); - - const resultData = (batchResult as any)?.data || batchResult; - typeResult.success += resultData.success || 0; - typeResult.failed += resultData.failed || 0; - - console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { - success: resultData.success || 0, - failed: resultData.failed || 0, - }); - } catch (error: any) { - console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { - error: error.message, - stack: error.stack, - batchSize: batch.length, - batchIndex: i, - profileType: profileType, - }); - - // Count all profiles in failed batch as failed - typeResult.failed += batch.length; - - // Optionally: Store failed batch for retry - // failedBatches.push({ index: i, batch, error: error.message }); - } -} -``` - ---- - -### 4. **SubDistrictController** - Generic Error Handling Without Logging - -**File & Location:** [SubDistrictController.ts](src/controllers/SubDistrictController.ts) - Method: `Delete()` - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -The delete operation uses a generic catch block without logging or differentiating between error types. This makes debugging difficult and may mask underlying issues. - -**Affected Code Locations:** -- Line 183-190: Generic catch block without error logging - -**Code Example:** - -```typescript -// Line 183-190 - Generic error handling -let result: any; -try { - result = await this.subDistrictRepository.delete({ id: id }); -} catch { - throw new HttpError( - HttpStatusCode.NOT_FOUND, - "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลแขวง/ตำบลนี้อยู่", - ); -} -``` - -**Recommended Fix:** - -```typescript -let result: any; -try { - result = await this.subDistrictRepository.delete({ id: id }); -} catch (error: any) { - console.error('Error deleting sub-district:', { - id, - error: error.message, - stack: error.stack, - code: error.code, - }); - - // Check for foreign key constraint error - if (error.code === 'ER_ROW_IS_REFERENCED_2' || - error.message?.includes('foreign key constraint') || - error.message?.includes('Cannot delete or update a parent row')) { - throw new HttpError( - HttpStatusCode.CONFLICT, - "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลแขวง/ตำบลนี้อยู่", - ); - } - - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - "เกิดข้อผิดพลาดในการลบข้อมูลแขวง/ตำบล", - ); -} -``` - ---- - -### 5. **UserController** - Promise.all Without Comprehensive Error Handling - -**File & Location:** [UserController.ts](src/controllers/UserController.ts) - Method: `listUserKeycloak()` - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -Complex database query with multiple conditions and joins without proper error handling. If the query fails or the database is unavailable, the error will propagate unhandled. - -**Affected Code Locations:** -- Line 566-608: Complex query builder operations for OFFICER type -- Line 610-653: Complex query builder operations for EMPLOYEE type - -**Code Example:** - -```typescript -// Line 566-608 - Complex query without error handling -[profiles, total] = await this.profileRepo - .createQueryBuilder("profile") - .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloaks") - .leftJoinAndSelect("profile.current_holders", "current_holders") - .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") - .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") - .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") - .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") - .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") - .andWhere("profile.isDelete = :isDelete", { isDelete: false }) - .andWhere(checkChildFromRole) - .andWhere(conditions) - .andWhere( - new Brackets((qb) => { - qb.orWhere( - body.keyword != null && body.keyword != "" - ? `profile.citizenId like '%${body.keyword}%'` - : "1=1", - ) - .orWhere( - body.keyword != null && body.keyword != "" - ? `profile.email like '%${body.keyword}%'` - : "1=1", - ) - .orWhere( - body.keyword != null && body.keyword != "" - ? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) like '%${body.keyword}%'` - : "1=1", - ); - }), - ) - .orderBy("profile.citizenId", "ASC") - .orderBy("orgRoot.orgRootOrder", "ASC") - .addOrderBy("orgChild1.orgChild1Order", "ASC") - .addOrderBy("orgChild2.orgChild2Order", "ASC") - .addOrderBy("orgChild3.orgChild3Order", "ASC") - .addOrderBy("orgChild4.orgChild4Order", "ASC") - .addOrderBy("current_holders.posMasterOrder", "ASC") - .addOrderBy("current_holders.posMasterCreatedAt", "ASC") - .skip((body.page - 1) * body.pageSize) - .take(body.pageSize) - .getManyAndCount(); -``` - -**Recommended Fix:** - -```typescript -let profiles: any = []; -let total: any; - -try { - if (body.type.trim().toUpperCase() == "OFFICER") { - try { - [profiles, total] = await this.profileRepo - .createQueryBuilder("profile") - .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloaks") - .leftJoinAndSelect("profile.current_holders", "current_holders") - .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") - .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") - .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") - .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") - .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") - .andWhere("profile.isDelete = :isDelete", { isDelete: false }) - .andWhere(checkChildFromRole) - .andWhere(conditions) - .andWhere( - new Brackets((qb) => { - qb.orWhere( - body.keyword != null && body.keyword != "" - ? `profile.citizenId like :citizenKeyword` - : "1=1", - { citizenKeyword: body.keyword ? `%${body.keyword}%` : '' }, - ) - .orWhere( - body.keyword != null && body.keyword != "" - ? `profile.email like :emailKeyword` - : "1=1", - { emailKeyword: body.keyword ? `%${body.keyword}%` : '' }, - ) - .orWhere( - body.keyword != null && body.keyword != "" - ? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) like :nameKeyword` - : "1=1", - { nameKeyword: body.keyword ? `%${body.keyword}%` : '' }, - ); - }), - ) - .orderBy("profile.citizenId", "ASC") - .addOrderBy("orgRoot.orgRootOrder", "ASC") - .addOrderBy("orgChild1.orgChild1Order", "ASC") - .addOrderBy("orgChild2.orgChild2Order", "ASC") - .addOrderBy("orgChild3.orgChild3Order", "ASC") - .addOrderBy("orgChild4.orgChild4Order", "ASC") - .addOrderBy("current_holders.posMasterOrder", "ASC") - .addOrderBy("current_holders.posMasterCreatedAt", "ASC") - .skip((body.page - 1) * body.pageSize) - .take(body.pageSize) - .getManyAndCount(); - } catch (error: any) { - console.error('Error querying officer profiles:', { - error: error.message, - stack: error.stack, - body: { ...body, keyword: body.keyword ? '[REDACTED]' : null }, - }); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to retrieve officer profiles' - ); - } - } else if (body.type.trim().toUpperCase() == "EMPLOYEE") { - try { - [profiles, total] = await this.profileEmpRepo - .createQueryBuilder("profileEmployee") - .leftJoinAndSelect("profileEmployee.roleKeycloaks", "roleKeycloaks") - .leftJoinAndSelect("profileEmployee.current_holders", "current_holders") - .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") - .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") - .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") - .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") - .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") - .andWhere("profileEmployee.isDelete = :isDelete", { isDelete: false }) - .andWhere(checkChildFromRole) - .andWhere(conditions) - .andWhere({ employeeClass: "PERM" }) - .andWhere( - new Brackets((qb) => { - qb.orWhere( - body.keyword != null && body.keyword != "" - ? `profileEmployee.citizenId like :citizenKeyword` - : "1=1", - { citizenKeyword: body.keyword ? `%${body.keyword}%` : '' }, - ) - .orWhere( - body.keyword != null && body.keyword != "" - ? `profileEmployee.email like :emailKeyword` - : "1=1", - { emailKeyword: body.keyword ? `%${body.keyword}%` : '' }, - ) - .orWhere( - body.keyword != null && body.keyword != "" - ? `CONCAT(profileEmployee.prefix, profileEmployee.firstName," ",profileEmployee.lastName) like :nameKeyword` - : "1=1", - { nameKeyword: body.keyword ? `%${body.keyword}%` : '' }, - ); - }), - ) - .orderBy("profileEmployee.citizenId", "ASC") - .addOrderBy("orgRoot.orgRootOrder", "ASC") - .addOrderBy("orgChild1.orgChild1Order", "ASC") - .addOrderBy("orgChild2.orgChild2Order", "ASC") - .addOrderBy("orgChild3.orgChild3Order", "ASC") - .addOrderBy("orgChild4.orgChild4Order", "ASC") - .addOrderBy("current_holders.posMasterOrder", "ASC") - .addOrderBy("current_holders.posMasterCreatedAt", "ASC") - .skip((body.page - 1) * body.pageSize) - .take(body.pageSize) - .getManyAndCount(); - } catch (error: any) { - console.error('Error querying employee profiles:', { - error: error.message, - stack: error.stack, - body: { ...body, keyword: body.keyword ? '[REDACTED]' : null }, - }); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to retrieve employee profiles' - ); - } - } -} catch (error) { - if (error instanceof HttpError) { - throw error; - } - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to retrieve user data' - ); -} - -const _profiles = profiles.map((_data: any) => ({ - id: _data.keycloak, - firstname: _data.firstName, - lastname: _data.lastName, - email: _data.email, - username: _data.citizenId, - citizenId: _data.citizenId, - roles: _data.roleKeycloaks, - enabled: _data.isActive, -})); -return new HttpSuccess({ data: _profiles, total }); -``` - ---- - -### 6. **SocketController** - No Error Handling for WebSocket Operations - -**File & Location:** [SocketController.ts](src/controllers/SocketController.ts) - Method: `notify()` - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -The WebSocket send operation has no error handling. If the WebSocket service fails, the error will be unhandled. - -**Affected Code Locations:** -- Line 7-24: Entire notify method - -**Code Example:** - -```typescript -@Post("notify") -async notify( - @Body() - payload: { - message: string; - userId?: string | string[]; - roles?: string | string[]; - error?: boolean; - }, -) { - sendWebSocket( - "socket-notification", - { success: !payload.error, message: payload.message }, - { - roles: payload.roles || [], - userId: payload.userId || [], - }, - ); -} -``` - -**Recommended Fix:** - -```typescript -@Post("notify") -async notify( - @Body() - payload: { - message: string; - userId?: string | string[]; - roles?: string | string[]; - error?: boolean; - }, -) { - try { - sendWebSocket( - "socket-notification", - { success: !payload.error, message: payload.message }, - { - roles: payload.roles || [], - userId: payload.userId || [], - }, - ); - return new HttpSuccess({ message: 'Notification sent successfully' }); - } catch (error: any) { - console.error('Error sending WebSocket notification:', { - message: payload.message, - userId: payload.userId, - roles: payload.roles, - error: error.message, - }); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to send notification' - ); - } -} -``` - ---- - -### 7. **RankController, RelationshipController, ReligionController** - No Error Handling - -**File & Location:** -- [RankController.ts](src/controllers/RankController.ts) -- [RelationshipController.ts](src/controllers/RelationshipController.ts) -- [ReligionController.ts](src/controllers/ReligionController.ts) - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -All database operations in these controllers lack proper error handling. While they use HttpError for business logic validation, they don't handle database errors (connection issues, timeouts, etc.). - -**Affected Code Locations:** -- All methods in all three controllers - -**Recommended Fix:** - -Add a generic error handling middleware or wrap each method with try-catch: - -```typescript -// Example for RankController -@Post() -async createRank( - @Body() - requestBody: CreateRank, - @Request() request: RequestWithUser, -) { - try { - const checkName = await this.rankRepository.findOne({ - where: { name: requestBody.name }, - }); - - if (checkName) { - throw new HttpError(HttpStatusCode.CONFLICT, "ชื่อนี้มีอยู่ในระบบแล้ว"); - } - - const before = null; - const rank = Object.assign(new Rank(), requestBody); - rank.createdUserId = request.user.sub; - rank.createdFullName = request.user.name; - rank.lastUpdateUserId = request.user.sub; - rank.lastUpdateFullName = request.user.name; - rank.createdAt = new Date(); - rank.lastUpdatedAt = new Date(); - - await this.rankRepository.save(rank, { data: request }); - setLogDataDiff(request, { before, after: rank }); - return new HttpSuccess(); - } catch (error) { - if (error instanceof HttpError) { - throw error; - } - console.error('Error creating rank:', { - name: requestBody.name, - error: error instanceof Error ? error.message : error, - stack: error instanceof Error ? error.stack : undefined, - }); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to create rank' - ); - } -} -``` - ---- - -### 8. **ViewWorkFlowController** - Sequential Async Operations in Loop - -**File & Location:** [ViewWorkFlowController.ts](src/controllers/ViewWorkFlowController.ts) - Method: `getSystems()` - -**Problem Type:** 2. Missing Error Handle - -**Root Cause:** -Uses a for loop to process items sequentially, which could be slow and doesn't handle errors for individual items. - -**Affected Code Locations:** -- Line 41-49: Sequential for loop processing - -**Code Example:** - -```typescript -const sys: any = []; - -for (let index = 0; index < lists.length; index++) { - const element = await lists[index]; - if (sys.findIndex((x: any) => x.sysName === element.sysName) === -1) { - sys.push({ - sysName: element.sysName, - name: element.name, - }); - } -} -``` - -**Recommended Fix:** - -```typescript -@Get("lists") -public async getSystems(@Request() req: RequestWithUser) { - try { - const lists = await this.metaWorkflowRepository - .createQueryBuilder("metaWorkflow") - .select(["metaWorkflow.name", "metaWorkflow.sysName"]) - .getMany(); - - // Use Map for better performance and automatic deduplication - const sysMap = new Map(); - - for (const element of lists) { - if (!sysMap.has(element.sysName)) { - sysMap.set(element.sysName, { - sysName: element.sysName, - name: element.name, - }); - } - } - - const sys = Array.from(sysMap.values()); - return new HttpSuccess(sys); - } catch (error: any) { - console.error('Error getting workflow systems:', { - error: error.message, - stack: error.stack, - }); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - 'Failed to retrieve workflow systems' - ); - } -} -``` - ---- - -## Summary Statistics - -**Total Critical Issues Found:** 8 - -**Breakdown by Type:** -- **Unhandled Exception (forEach/for await with async):** 5 instances -- **Missing Error Handling (DB operations):** 10 instances -- **Transaction Issues:** 1 instance -- **External API Error Handling:** 2 instances -- **Generic Error Handling:** 3 instances - -**Controllers with Issues:** -1. UserController - 5 critical issues -2. WorkflowController - 2 critical issues -3. ScriptProfileOrgController - 2 critical issues -4. SubDistrictController - 1 issue -5. SocketController - 1 issue -6. RankController - 1 issue -7. RelationshipController - 1 issue -8. ReligionController - 1 issue -9. ViewWorkFlowController - 1 issue -10. ReportController - Not fully analyzed (file too large) - -**Risk Level: HIGH** - ---- - -## Priority Recommendations - -### Immediate Actions Required: - -1. **Fix UserController methods** - Add comprehensive error handling to all `for await` loops and Keycloak operations -2. **Add transactions to WorkflowController** - Ensure data consistency during workflow creation -3. **Improve external API error handling** - Add proper logging and retry logic for external service calls -4. **Add global error handling middleware** - Catch unhandled errors at the application level -5. **Implement circuit breakers** - For external dependencies (Keycloak, leave service) - -### Graceful Recovery Strategies: - -1. **Implement request-level error boundaries** - Catch errors at the controller level -2. **Add operation timeouts** - Prevent indefinite hangs on external API calls -3. **Implement retry logic with exponential backoff** - For transient failures -4. **Add health checks** - Monitor Keycloak and 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 distributed issues -4. **Add metrics/monitoring** for error rates and external API failures -5. **Implement graceful shutdown** procedures for batch operations - ---- - -## Testing Recommendations - -1. **Test Keycloak failure scenarios** - Simulate Keycloak unavailability during user operations -2. **Test with large datasets** - Ensure for await 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 external API failures** - Simulate leave service and notification failures -6. **Test database connection failures** - Ensure proper handling of connection issues diff --git a/sql_seed/update_profile_position_fields.sql b/sql_seed/update_profile_position_fields.sql deleted file mode 100644 index e9e999dc..00000000 --- a/sql_seed/update_profile_position_fields.sql +++ /dev/null @@ -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; diff --git a/src/app.ts b/src/app.ts index c2cbe5f7..06f76548 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,7 +19,6 @@ import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgContro import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; -import { RetirementService } from "./services/RetirementService"; async function main() { 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}`)); const server = app.listen( APP_PORT, diff --git a/src/controllers/ApiManageController.ts b/src/controllers/ApiManageController.ts index 1f89e98d..01a27eb5 100644 --- a/src/controllers/ApiManageController.ts +++ b/src/controllers/ApiManageController.ts @@ -316,153 +316,16 @@ export class ApiManageController extends Controller { description: "ข้อมูลส่วนราชการ ระดับที่ 4", system: ["position"], }, - // { - // name: "Profile", - // repository: this.profileRepository, - // description: "ข้อมูลคนครอง", - // system: ["position"], - // }, + { + name: "Profile", + repository: this.profileRepository, + description: "ข้อมูลคนครอง", + system: ["position"], + }, ]; private readonly DEFAULT_PAGE_SIZE = 10; // ขนาดหน้าเริ่มต้น - private readonly EXCLUDED_COLUMNS = [ - "createdUserId", - "lastUpdateUserId", - "createdAt", - "createdFullName", - "lastUpdateFullName", - "avatarName", - "profileId", - "prefixId", - "profileEmployeeId", - "documentId", - "orgRevisionId", - "posMasterId", - "orgRootId", - "orgChild1Id", - "orgChild2Id", - "orgChild3Id", - "orgChild4Id", - "ancestorDNA", - "keycloak", - "commandId", - "prefixMain", - "authRoleId", - "next_holderId", - "current_holderId", - ]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์ - - // การแทนที่ฟิลด์ 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", - }, - }; + private readonly EXCLUDED_COLUMNS = ["createdUserId", "lastUpdateUserId"]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์ private validateSuperAdminRole(user: any): void { if (!user.role.includes("SUPER_ADMIN")) { @@ -501,8 +364,11 @@ export class ApiManageController extends Controller { const result = this.entities .filter((entity) => entity.system.includes(system)) - .map(({ name, repository, description, isMain }) => { - let columns = repository.metadata.columns + .map(({ name, repository, description, isMain }) => ({ + tb: name, + description, + isMain: isMain || false, + propertys: repository.metadata.columns .filter( (column: any) => !column.isPrimary && !this.EXCLUDED_COLUMNS.includes(column.propertyName), @@ -512,94 +378,8 @@ export class ApiManageController extends Controller { type: typeof column.type === "string" ? column.type : "string", comment: column.comment, key: column.propertyName, - })); - - // Special handling for Profile entity - replace ID fields with name fields - if (name === "Profile") { - const replacementKeys = Object.keys(this.PROFILE_FIELD_REPLACEMENTS); - - // Remove ID fields that should be replaced - columns = columns.filter( - (col: { propertyName: string }) => !replacementKeys.includes(col.propertyName), - ); - - // Add the corresponding name fields - const nameFields = replacementKeys.map((key) => ({ - propertyName: this.PROFILE_FIELD_REPLACEMENTS[key].propertyName, - type: "string", - comment: this.PROFILE_FIELD_REPLACEMENTS[key].comment, - key: this.PROFILE_FIELD_REPLACEMENTS[key].propertyName, - })); - - columns = [...columns, ...nameFields]; - } - - // Special handling for Position entity - replace ID fields with name fields - if (name === "Position") { - const replacementKeys = Object.keys(this.POSITION_FIELD_REPLACEMENTS); - - // Remove ID fields that should be replaced - columns = columns.filter( - (col: { propertyName: string }) => !replacementKeys.includes(col.propertyName), - ); - - // Add the corresponding name fields - const nameFields = replacementKeys.map((key) => ({ - propertyName: this.POSITION_FIELD_REPLACEMENTS[key].propertyName, - type: "string", - comment: this.POSITION_FIELD_REPLACEMENTS[key].comment, - key: this.POSITION_FIELD_REPLACEMENTS[key].propertyName, - })); - - columns = [...columns, ...nameFields]; - } - - // Special handling for ProfileEmployee entity - replace ID fields with name fields - if (name === "ProfileEmployee") { - const replacementKeys = Object.keys(this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS); - - // Remove ID fields that should be replaced - columns = columns.filter( - (col: { propertyName: string }) => !replacementKeys.includes(col.propertyName), - ); - - // Add the corresponding name fields - const nameFields = replacementKeys.map((key) => ({ - propertyName: this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].propertyName, - type: "string", - comment: this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].comment, - key: this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].propertyName, - })); - - columns = [...columns, ...nameFields]; - } - - // Special handling for PosMaster entity - add Profile fields for holder information - if (name === "PosMaster") { - // Add Profile fields that are accessible via current_holder relation - const profileFields = ["prefix", "rank", "firstName", "lastName", "citizenId"]; - const profileRepository = AppDataSource.getRepository(Profile); - const profileColumns = profileRepository.metadata.columns - .filter( - (column: any) => !column.isPrimary && profileFields.includes(column.propertyName), - ) - .map((column: any) => ({ - propertyName: `Profile.${column.propertyName}`, - type: typeof column.type === "string" ? column.type : "string", - comment: column.comment, - key: `Profile.${column.propertyName}`, - })); - - columns = [...columns, ...profileColumns]; - } - - return { - tb: name, - description, - isMain: isMain || false, - propertys: columns, - }; - }); + })), + })); return new HttpSuccess(result); } catch (error) { diff --git a/src/controllers/ApiWebServiceController.ts b/src/controllers/ApiWebServiceController.ts index 8673e684..61f3d54a 100644 --- a/src/controllers/ApiWebServiceController.ts +++ b/src/controllers/ApiWebServiceController.ts @@ -8,10 +8,6 @@ import { isPermissionRequest } from "../middlewares/authWebService"; import { RequestWithUserWebService } from "../middlewares/user"; import { OrgRevision } from "../entities/OrgRevision"; import { ApiHistory } from "../entities/ApiHistory"; -import { OrgChild1 } from "../entities/OrgChild1"; -import { OrgChild2 } from "../entities/OrgChild2"; -import { OrgChild3 } from "../entities/OrgChild3"; -import { OrgChild4 } from "../entities/OrgChild4"; import { SystemCode } from "./../interfaces/api-type"; @Route("api/v1/org/api-service") @Tags("ApiKey") @@ -24,170 +20,6 @@ export class ApiWebServiceController extends Controller { private apiNameRepository = AppDataSource.getRepository(ApiName); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private apiHistoryRepository = AppDataSource.getRepository(ApiHistory); - private currentRevisionId: string = ""; - - // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity - private readonly PROFILE_FIELD_REPLACEMENTS: Record< - string, - { propertyName: string; joinRelation: string; joinField: string } - > = { - posLevelName: { - propertyName: "posLevelId", - joinRelation: "posLevel", - joinField: "posLevelName", - }, - posTypeName: { - propertyName: "posTypeId", - joinRelation: "posType", - joinField: "posTypeName", - }, - registrationProvinceName: { - propertyName: "registrationProvinceId", - joinRelation: "registrationProvince", - joinField: "name", - }, - registrationDistrictName: { - propertyName: "registrationDistrictId", - joinRelation: "registrationDistrict", - joinField: "name", - }, - registrationSubDistrictName: { - propertyName: "registrationSubDistrictId", - joinRelation: "registrationSubDistrict", - joinField: "name", - }, - currentProvinceName: { - propertyName: "currentProvinceId", - joinRelation: "currentProvince", - joinField: "name", - }, - currentDistrictName: { - propertyName: "currentDistrictId", - joinRelation: "currentDistrict", - joinField: "name", - }, - currentSubDistrictName: { - propertyName: "currentSubDistrictId", - joinRelation: "currentSubDistrict", - joinField: "name", - }, - }; - - // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Position entity - private readonly POSITION_FIELD_REPLACEMENTS: Record< - string, - { propertyName: string; joinRelation: string; joinField: string } - > = { - posTypeName: { - propertyName: "posTypeId", - joinRelation: "posType", - joinField: "posTypeName", - }, - posLevelName: { - propertyName: "posLevelId", - joinRelation: "posLevel", - joinField: "posLevelName", - }, - posExecutiveName: { - propertyName: "posExecutiveId", - joinRelation: "posExecutive", - joinField: "posExecutiveName", - }, - }; - - // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ ProfileEmployee entity - private readonly PROFILEEMPLOYEE_FIELD_REPLACEMENTS: Record< - string, - { propertyName: string; joinRelation: string; joinField: string } - > = { - posTypeName: { - propertyName: "posTypeId", - joinRelation: "posType", - joinField: "posTypeName", - }, - posLevelName: { - propertyName: "posLevelId", - joinRelation: "posLevel", - joinField: "posLevelName", - }, - }; - - /** - * build posMaster permission condition - * @summary สร้างเงื่อนไขการกรองข้อมูลตามสิทธิ์การเข้าถึง - */ - private buildPosMasterPermissionCondition( - accessType: string | undefined, - dnaIds: { - dnaRootId?: string | null; - dnaChild1Id?: string | null; - dnaChild2Id?: string | null; - dnaChild3Id?: string | null; - dnaChild4Id?: string | null; - }, - tableAlias: string = "posMaster", - ): string { - // ALL - no filtering - if (accessType === "ALL") { - return "1=1"; - } - - // No access type specified but has DNA IDs - default to NORMAL behavior - const conditions: string[] = []; - - if (accessType === "ROOT" && dnaIds.dnaRootId) { - // All organizations under this root - conditions.push( - `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%")`, - ); - } else if (accessType === "CHILD" || accessType === "NORMAL") { - // Build conditions based on which DNA level is specified - if (dnaIds.dnaChild4Id) { - conditions.push( - `${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild4Id}")`, - ); - } else if (dnaIds.dnaChild3Id) { - conditions.push( - `${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}")`, - ); - // For CHILD type, include all descendants - if (accessType === "CHILD") { - conditions.push( - `(${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild3Id}%") OR ${tableAlias}.orgChild4Id IS NOT NULL)`, - ); - } - } else if (dnaIds.dnaChild2Id) { - conditions.push( - `${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`, - ); - if (accessType === "CHILD") { - conditions.push( - `(${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%") OR ${tableAlias}.orgChild3Id IS NOT NULL)`, - ); - } - } else if (dnaIds.dnaChild1Id) { - conditions.push( - `${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`, - ); - if (accessType === "CHILD") { - conditions.push( - `(${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR ${tableAlias}.orgChild2Id IS NOT NULL)`, - ); - } - } else if (dnaIds.dnaRootId) { - conditions.push( - `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`, - ); - if (accessType === "CHILD") { - conditions.push( - `(${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild1Id IS NOT NULL)`, - ); - } - } - } - - return conditions.length > 0 ? `(${conditions.join(" OR ")})` : "1=1"; - } /** * list fields by systems @@ -218,14 +50,7 @@ export class ApiWebServiceController extends Controller { } await isPermissionRequest(request, apiName.id); const offset = (page - 1) * pageSize; - let propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`); - const selectedFieldsByTable: Record> = {}; - apiName.apiAttributes.forEach((attr) => { - if (!selectedFieldsByTable[attr.tbName]) { - selectedFieldsByTable[attr.tbName] = new Set(); - } - selectedFieldsByTable[attr.tbName].add(attr.propertyKey); - }); + const propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`); let tbMain: string = ""; let condition: string = "1=1"; @@ -253,104 +78,6 @@ export class ApiWebServiceController extends Controller { condition = `PosMaster.orgRevisionId = "${revision?.id}"`; } - let posMasterCondition: string = "1=1"; - let posMasterAlias: string = ""; - - // Special handling for Profile and ProfileEmployee systems with permission filtering - if (system == "registry") { - // Get current revision - const revision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - // Store for use in permission building - this.currentRevisionId = revision?.id || ""; - posMasterAlias = "posMaster"; - - // Build permission condition - posMasterCondition = this.buildPosMasterPermissionCondition( - request.user.accessType, - { - dnaRootId: request.user.dnaRootId, - dnaChild1Id: request.user.dnaChild1Id, - dnaChild2Id: request.user.dnaChild2Id, - dnaChild3Id: request.user.dnaChild3Id, - dnaChild4Id: request.user.dnaChild4Id, - }, - posMasterAlias, - ); - } else if (system == "registry_emp") { - // Get current revision - const revision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - // Store for use in permission building - this.currentRevisionId = revision?.id || ""; - posMasterAlias = "employeePosMaster"; - - // Build permission condition - posMasterCondition = this.buildPosMasterPermissionCondition( - request.user.accessType, - { - dnaRootId: request.user.dnaRootId, - dnaChild1Id: request.user.dnaChild1Id, - dnaChild2Id: request.user.dnaChild2Id, - dnaChild3Id: request.user.dnaChild3Id, - dnaChild4Id: request.user.dnaChild4Id, - }, - posMasterAlias, - ); - } else if (system == "registry_temp") { - // Get current revision - const revision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - // Store for use in permission building - this.currentRevisionId = revision?.id || ""; - posMasterAlias = "employeeTempPosMaster"; - - // Build permission condition - posMasterCondition = this.buildPosMasterPermissionCondition( - request.user.accessType, - { - dnaRootId: request.user.dnaRootId, - dnaChild1Id: request.user.dnaChild1Id, - dnaChild2Id: request.user.dnaChild2Id, - dnaChild3Id: request.user.dnaChild3Id, - dnaChild4Id: request.user.dnaChild4Id, - }, - posMasterAlias, - ); - } else if (system == "position") { - // Get current revision - const revision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - // Store for use in permission building - this.currentRevisionId = revision?.id || ""; - posMasterAlias = "PosMaster"; // Note: Uses PascalCase to match tbMain alias - - // Build permission condition - posMasterCondition = this.buildPosMasterPermissionCondition( - request.user.accessType, - { - dnaRootId: request.user.dnaRootId, - dnaChild1Id: request.user.dnaChild1Id, - dnaChild2Id: request.user.dnaChild2Id, - dnaChild3Id: request.user.dnaChild3Id, - dnaChild4Id: request.user.dnaChild4Id, - }, - posMasterAlias, - ); - } - const repo = AppDataSource.getRepository(tbMain); const metadata = repo.metadata; @@ -365,182 +92,27 @@ export class ApiWebServiceController extends Controller { ...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)), ]; - // Organization hierarchy is assembled in a separate step; keep main query focused on OrgRoot only. - if (tbMain === "OrgRoot") { - const orgChildTables = new Set(["OrgChild1", "OrgChild2", "OrgChild3", "OrgChild4"]); - propertyKey = propertyKey.filter((key) => !orgChildTables.has(key.split(".")[0])); - propertyOtherKey = propertyOtherKey.filter((tb) => !orgChildTables.has(tb)); - } - - // สำหรับ Profile: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey - const profileFieldJoins: Record = {}; // alias -> relationName - if (tbMain === "Profile") { - propertyKey = propertyKey.map((key) => { - const [table, field] = key.split("."); - if (table === "Profile") { - const replacement = this.PROFILE_FIELD_REPLACEMENTS[field]; - if (replacement) { - const alias = `${table}_${replacement.joinRelation}`; - profileFieldJoins[alias] = replacement.joinRelation; - return `${alias}.${replacement.joinField}`; - } - } - return key; - }); - } - - // สำหรับ Position: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey - const positionFieldJoins: Record = {}; // alias -> relationName - if (tbMain === "Position" || tbMain === "PosMaster") { - propertyKey = propertyKey.map((key) => { - const [table, field] = key.split("."); - if (table === "Position") { - const replacement = this.POSITION_FIELD_REPLACEMENTS[field]; - if (replacement) { - const alias = `${table}_${replacement.joinRelation}`; - positionFieldJoins[alias] = replacement.joinRelation; - return `${alias}.${replacement.joinField}`; - } - } - return key; - }); - } - - // สำหรับ ProfileEmployee: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey - const profileEmployeeFieldJoins: Record = {}; // alias -> relationName - if (tbMain === "ProfileEmployee") { - propertyKey = propertyKey.map((key) => { - const [table, field] = key.split("."); - if (table === "ProfileEmployee") { - const replacement = this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[field]; - if (replacement) { - const alias = `${table}_${replacement.joinRelation}`; - profileEmployeeFieldJoins[alias] = replacement.joinRelation; - return `${alias}.${replacement.joinField}`; - } - } - return key; - }); - } - const queryBuilder = repo.createQueryBuilder(tbMain); // join กับตารารอง if (propertyOtherKey.length > 0) { propertyOtherKey.forEach((tb) => { - // Skip Profile join for PosMaster - it's handled separately below - if (tbMain === "PosMaster" && tb === "Profile") { - return; - } - - // Skip Position join for PosMaster - it's handled separately below - if (tbMain === "PosMaster" && tb === "Position") { - return; - } - const relationName = relationMap[tb]; if (relationName) { - queryBuilder.leftJoin(`${tbMain}.${relationName}`, tb); - } else { - // Remove fields from this table from propertyKey - propertyKey = propertyKey.filter((key) => !key.startsWith(`${tb}.`)); - } - }); - } - - // Check if propertyKey is empty after filtering - if (propertyKey.length === 0) { - throw new HttpError( - HttpStatusCode.BAD_REQUEST, - "ไม่พบฟิลด์ที่ต้องการแสดงผล กรุณาตรวจสอบการตั้งค่า API (ไม่สามารถ join ตารางลูกได้)", - ); - } - - // join สำหรับฟิลด์ Profile ที่ต้องการดึงค่าจากตารางอื่น - if (tbMain === "Profile" && Object.keys(profileFieldJoins).length > 0) { - Object.entries(profileFieldJoins).forEach(([alias, relationName]) => { - queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); - }); - } - - // join สำหรับฟิลด์ Position ที่ต้องการดึงค่าจากตารางอื่น - if ( - (tbMain === "Position" || tbMain === "PosMaster") && - Object.keys(positionFieldJoins).length > 0 - ) { - if (tbMain === "PosMaster") { - const posMasterPositionRelation = relationMap["Position"]; - if (!posMasterPositionRelation) { - throw new HttpError( - HttpStatusCode.BAD_REQUEST, - "ไม่พบความสัมพันธ์ระหว่าง PosMaster กับ Position กรุณาตรวจสอบการตั้งค่า API", + queryBuilder.leftJoin( + `${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`, // เช็คว่าถ้าเป็น next_holder ให้ใช้ current_holder แทน + tb, ); } - - // Join PosMaster -> Position once using actual relation name from metadata - queryBuilder.leftJoin(`PosMaster.${posMasterPositionRelation}`, "Position"); - } - - Object.entries(positionFieldJoins).forEach(([alias, relationName]) => { - if (tbMain === "PosMaster") { - queryBuilder.leftJoin(`Position.${relationName}`, alias); - } else { - queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); - } }); } - // join สำหรับฟิลด์ ProfileEmployee ที่ต้องการดึงค่าจากตารางอื่น - if (tbMain === "ProfileEmployee" && Object.keys(profileEmployeeFieldJoins).length > 0) { - Object.entries(profileEmployeeFieldJoins).forEach(([alias, relationName]) => { - queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); - }); - } - - // join สำหรับ PosMaster เมื่อต้องการดึงค่าจาก Profile (ข้อมูลคนครอง) - const posMasterProfileFields: string[] = []; - if (tbMain === "PosMaster") { - // Collect Profile fields from both formats: "Profile.xxx" and "PosMaster.Profile.xxx" - const extractedProfileFields = propertyKey - .filter((key) => key.startsWith("Profile.") || key.startsWith("PosMaster.Profile.")) - .map((key) => key.replace(/^PosMaster\.Profile\./, "Profile.")); - - posMasterProfileFields.push(...new Set(extractedProfileFields)); - - // Remove Profile fields then add back normalized "Profile.xxx" form - propertyKey = propertyKey.filter( - (key) => !key.startsWith("Profile.") && !key.startsWith("PosMaster.Profile."), - ); - propertyKey.push(...posMasterProfileFields); - } - - // join PosMaster กับ Profile เมื่อมีการขอ Profile fields - if (tbMain === "PosMaster" && posMasterProfileFields.length > 0) { - // Always join via current_holder (not next_holder) because PosMaster has two relations - // to Profile and relationMap["Profile"] would resolve to next_holder (last defined in entity) - queryBuilder.leftJoin("PosMaster.current_holder", "Profile"); - } - - // join กับ posMaster/employeePosMaster/employeeTempPosMaster เพื่อกรองตามสิทธิ์การเข้าถึง - if ((tbMain === "Profile" || tbMain === "ProfileEmployee") && posMasterCondition !== "1=1") { - if (tbMain === "Profile") { - queryBuilder.leftJoin("Profile.current_holders", "posMaster"); - } else if (tbMain === "ProfileEmployee") { - // Use the correct relation based on posMasterAlias - if (posMasterAlias === "employeeTempPosMaster") { - queryBuilder.leftJoin("ProfileEmployee.current_holderTemps", "employeeTempPosMaster"); - } else { - queryBuilder.leftJoin("ProfileEmployee.current_holders", "employeePosMaster"); - } - } - } - // // เพิ่ม Main.id เพราะจะใช้ pk ในการแมบและนับจำนวน // if (!propertyKey.includes(`${Main}.id`)) { // propertyKey.push(`${Main}.id`); // } - // add PK - ensure propertyKey is never empty + // add FK let pk: string = ""; const primaryColumns = metadata.primaryColumns; primaryColumns.forEach((col) => { @@ -550,27 +122,13 @@ export class ApiWebServiceController extends Controller { } }); - let items: any[] = []; - let total = 0; - - if (tbMain === "OrgRoot") { - // Organization API should always return full hierarchy regardless of page/pageSize. - [items, total] = await queryBuilder - .select(propertyKey) - .where(condition) - .andWhere(posMasterCondition) - .orderBy(propertyKey[0] || `${tbMain}.${pk}`, "ASC") - .getManyAndCount(); - } else { - [items, total] = await queryBuilder - .select(propertyKey) - .where(condition) - .andWhere(posMasterCondition) - .orderBy(propertyKey[0] || `${tbMain}.${pk}`, "ASC") - .skip(offset) - .take(pageSize) - .getManyAndCount(); - } + const [items, total] = await queryBuilder + .select(propertyKey) + .where(condition) + .orderBy(propertyKey[0], "ASC") + .skip(offset) + .take(pageSize) + .getManyAndCount(); // ลบ Main.id // const results = items.map(({ id, ...x }) => x); @@ -583,229 +141,11 @@ export class ApiWebServiceController extends Controller { // split object id ออกก่อน return const data = items.map((item) => { - const { [pk]: removedPk, ...rest } = item; - - // สำหรับ Profile: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม - if (tbMain === "Profile") { - const flattened: any = { ...rest }; - Object.entries(this.PROFILE_FIELD_REPLACEMENTS).forEach(([nameField, config]) => { - const alias = `${tbMain}_${config.joinRelation}`; - if (rest[alias] && rest[alias][config.joinField] !== undefined) { - flattened[nameField] = rest[alias][config.joinField]; - delete flattened[alias]; - } - }); - return flattened; - } - - // สำหรับ Position: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม - if (tbMain === "Position" || tbMain === "PosMaster") { - const flattened: any = { ...rest }; - Object.entries(this.POSITION_FIELD_REPLACEMENTS).forEach(([nameField, config]) => { - // Remove the original ID field - delete flattened[config.propertyName]; - // Add the name field from joined table - const alias = `Position_${config.joinRelation}`; - if (rest[alias] && rest[alias][config.joinField] !== undefined) { - flattened[nameField] = rest[alias][config.joinField]; - } - // Remove the joined table object - delete flattened[alias]; - }); - // Remove Position object if exists - if (flattened["Position"]) { - delete flattened["Position"]; - } - return flattened; - } - - // สำหรับ ProfileEmployee: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม - if (tbMain === "ProfileEmployee") { - const flattened: any = { ...rest }; - Object.entries(this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS).forEach(([nameField, config]) => { - // Remove the original ID field - delete flattened[config.propertyName]; - // Add the name field from joined table - const alias = `${tbMain}_${config.joinRelation}`; - if (rest[alias] && rest[alias][config.joinField] !== undefined) { - flattened[nameField] = rest[alias][config.joinField]; - } - // Remove the joined table object - delete flattened[alias]; - }); - return flattened; - } - - // สำหรับ PosMaster: แปลงฟิลด์ Profile ที่มาจาก join กลับเป็นฟิลด์ระดับบน - if (tbMain === "PosMaster" && posMasterProfileFields.length > 0) { - const flattened: any = { ...rest }; - const profileFieldNames = posMasterProfileFields - .filter((field) => field.startsWith("Profile.")) - .map((field) => field.replace("Profile.", "")); - - // Extract only requested Profile fields and add top-level aliases - if (rest["Profile"]) { - profileFieldNames.forEach((fieldName) => { - if (rest["Profile"][fieldName] !== undefined) { - flattened[`profile_${fieldName}`] = rest["Profile"][fieldName]; - } - }); - // Remove the nested Profile object - delete flattened["Profile"]; - } - return flattened; - } - - // สำหรับ OrgRoot: เก็บ primary key ไว้ใช้ group ข้อมูล แล้วแยก children ภายหลัง - if (tbMain === "OrgRoot") { - return { __rootPk: removedPk, ...rest }; - } - - return rest; + const { [pk]: removedPk, ...x } = item; + return x; }); - let responseData: any[] = data; - let responseTotal = total; - - // สำหรับ Organization: รวมข้อมูลให้เหลือ 1 root ต่อ 1 object และจัด children ตาม hierarchy - if (tbMain === "OrgRoot") { - const rootVisibleFields = Array.from(selectedFieldsByTable["OrgRoot"] || []); - const child1VisibleFields = Array.from(selectedFieldsByTable["OrgChild1"] || []); - const child2VisibleFields = Array.from(selectedFieldsByTable["OrgChild2"] || []); - const child3VisibleFields = Array.from(selectedFieldsByTable["OrgChild3"] || []); - const child4VisibleFields = Array.from(selectedFieldsByTable["OrgChild4"] || []); - - const pickVisibleFields = (obj: any, fields: string[]) => { - const out: any = {}; - fields.forEach((field) => { - if (obj[field] !== undefined) { - out[field] = obj[field]; - } - }); - return out; - }; - - const rootMap = new Map(); - data.forEach((row: any) => { - if (!row.__rootPk || rootMap.has(row.__rootPk)) { - return; - } - - const rootNode = { - ...pickVisibleFields(row, rootVisibleFields), - children: [], - }; - rootMap.set(row.__rootPk, rootNode); - }); - - const rootIds = Array.from(rootMap.keys()); - - if (rootIds.length > 0) { - const buildSelect = (alias: string, required: string[], visible: string[]) => - Array.from(new Set([...required, ...visible])).map((field) => `${alias}.${field}`); - - const [child1Rows, child2Rows, child3Rows, child4Rows] = await Promise.all([ - AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("OrgChild1") - .select(buildSelect("OrgChild1", ["id", "orgRootId"], child1VisibleFields)) - .where("OrgChild1.orgRootId IN (:...rootIds)", { rootIds }) - .orderBy("OrgChild1.id", "ASC") - .getMany(), - AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("OrgChild2") - .select( - buildSelect("OrgChild2", ["id", "orgRootId", "orgChild1Id"], child2VisibleFields), - ) - .where("OrgChild2.orgRootId IN (:...rootIds)", { rootIds }) - .orderBy("OrgChild2.id", "ASC") - .getMany(), - AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("OrgChild3") - .select( - buildSelect( - "OrgChild3", - ["id", "orgRootId", "orgChild1Id", "orgChild2Id"], - child3VisibleFields, - ), - ) - .where("OrgChild3.orgRootId IN (:...rootIds)", { rootIds }) - .orderBy("OrgChild3.id", "ASC") - .getMany(), - AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("OrgChild4") - .select( - buildSelect( - "OrgChild4", - ["id", "orgRootId", "orgChild1Id", "orgChild2Id", "orgChild3Id"], - child4VisibleFields, - ), - ) - .where("OrgChild4.orgRootId IN (:...rootIds)", { rootIds }) - .orderBy("OrgChild4.id", "ASC") - .getMany(), - ]); - - const child1Map = new Map(); - const child2Map = new Map(); - const child3Map = new Map(); - - child1Rows.forEach((row) => { - const node = { - ...pickVisibleFields(row, child1VisibleFields), - children: [], - }; - child1Map.set(row.id, node); - - const rootNode = rootMap.get(row.orgRootId); - if (rootNode) { - rootNode.children.push(node); - } - }); - - child2Rows.forEach((row) => { - const node = { - ...pickVisibleFields(row, child2VisibleFields), - children: [], - }; - child2Map.set(row.id, node); - - const parent = child1Map.get(row.orgChild1Id); - if (parent) { - parent.children.push(node); - } - }); - - child3Rows.forEach((row) => { - const node = { - ...pickVisibleFields(row, child3VisibleFields), - children: [], - }; - child3Map.set(row.id, node); - - const parent = child2Map.get(row.orgChild2Id); - if (parent) { - parent.children.push(node); - } - }); - - child4Rows.forEach((row) => { - const node = { - ...pickVisibleFields(row, child4VisibleFields), - }; - - const parent = child3Map.get(row.orgChild3Id); - if (parent) { - if (!Array.isArray(parent.children)) { - parent.children = []; - } - parent.children.push(node); - } - }); - } - - responseData = Array.from(rootMap.values()); - responseTotal = responseData.length; - } + // console.log("queryBuilder ===> ", queryBuilder.getQuery()); // save api history after query success const history = { @@ -855,6 +195,6 @@ export class ApiWebServiceController extends Controller { // return flattenedItem; // }); - return new HttpSuccess({ data: responseData, total: responseTotal }); + return new HttpSuccess({ data: data, total }); } } diff --git a/src/controllers/AuthRoleController.ts b/src/controllers/AuthRoleController.ts index 13f5ce04..4159c5ec 100644 --- a/src/controllers/AuthRoleController.ts +++ b/src/controllers/AuthRoleController.ts @@ -123,25 +123,18 @@ export class AuthRoleController extends Controller { // เช็คว่าถ้ามีค่า current_holderId ให้ลบ key สิทธิ์ใน redis if (posMaster.current_holderId) { - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); - redisClient.del("role_" + posMaster.current_holderId, (err: Error) => { - if (err) console.error("Redis delete role error:", err); - }); + redisClient.del("role_" + posMaster.current_holderId, (err: Error, response: Response) => { + if (err) throw err; + }); - redisClient.del("menu_" + posMaster.current_holderId, (err: Error) => { - if (err) console.error("Redis delete menu error:", err); - }); - } finally { - if (redisClient) { - redisClient.quit(); - } - } + redisClient.del("menu_" + posMaster.current_holderId, (err: Error, response: Response) => { + if (err) throw err; + }); } return new HttpSuccess(); @@ -267,45 +260,20 @@ export class AuthRoleController extends Controller { return newAttr; }); const before = structuredClone(record); + await Promise.all([ + this.authRoleRepo.save(record, { data: req }), + setLogDataDiff(req, { before, after: record }), + ...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)), + ]); - const queryRunner = AppDataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); - try { - await queryRunner.manager.save(AuthRole, record); - await Promise.all( - newAttrs.map((attr) => queryRunner.manager.save(AuthRoleAttr, attr)) - ); - await queryRunner.commitTransaction(); - - setLogDataDiff(req, { before, after: record }); - } catch (error) { - await queryRunner.rollbackTransaction(); - console.error("Error saving auth role:", error); - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - "เกิดข้อผิดพลาดในการบันทึกข้อมูลบทบาท กรุณาลองใหม่ในภายหลัง" - ); - } finally { - await queryRunner.release(); - } - - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - - await redisClient.flushdb(function (err: any, succeeded: any) { - console.log(succeeded); // will be true if successfull - }); - } finally { - if (redisClient) { - redisClient.quit(); - } - } + await redisClient.flushdb(function (err: any, succeeded: any) { + console.log(succeeded); // will be true if successfull + }); return new HttpSuccess(); } diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 42685c20..da0ed3fe 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -48,7 +48,6 @@ import { import { Position } from "../entities/Position"; import { PosMaster } from "../entities/PosMaster"; import { EmployeePosition } from "../entities/EmployeePosition"; -import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { ProfileDiscipline } from "../entities/ProfileDiscipline"; import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory"; @@ -100,14 +99,10 @@ import { CreatePosMasterHistoryEmployeeTemp, CreatePosMasterHistoryOfficer, } from "../services/PositionService"; +import { PostRetireToExprofile } from "./ExRetirementController"; import { LeaveType } from "../entities/LeaveType"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; -import { RetirementService } from "../services/RetirementService"; -import { promisify } from "util"; -const REDIS_HOST = process.env.REDIS_HOST; -const REDIS_PORT = process.env.REDIS_PORT; - @Route("api/v1/org/command") @Tags("Command") @Security("bearerAuth") @@ -116,7 +111,6 @@ const REDIS_PORT = process.env.REDIS_PORT; "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการได้ กรุณาลองใหม่ในภายหลัง", ) export class CommandController extends Controller { - private redis = require("redis"); private commandRepository = AppDataSource.getRepository(Command); private commandTypeRepository = AppDataSource.getRepository(CommandType); private commandSendRepository = AppDataSource.getRepository(CommandSend); @@ -1613,7 +1607,8 @@ export class CommandController extends Controller { return new HttpSuccess(); } - async cronjobUpdateRetirementStatus() { + // @Get("XXX") + async cronjobUpdateRetirementStatus(/*@Request() request: RequestWithUser*/) { const adminToken = (await getToken()) ?? ""; const today = new Date(); today.setUTCHours(0, 0, 0, 0); @@ -1658,11 +1653,7 @@ export class CommandController extends Controller { _profile.leaveDate = _Date; _profile.dateLeave = _Date; _profile.lastUpdatedAt = _Date; - if ( - _profile.keycloak != null && - _profile.keycloak != "" && - _profile.isDelete === false - ) { + if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { // console.log("4. disable keycloak/authen") const delUserKeycloak = await deleteUser(_profile.keycloak, adminToken); if (delUserKeycloak) { @@ -1720,11 +1711,7 @@ export class CommandController extends Controller { _profileEmp.leaveDate = _Date; _profileEmp.dateLeave = _Date; _profileEmp.lastUpdatedAt = _Date; - if ( - _profileEmp.keycloak != null && - _profileEmp.keycloak != "" && - _profileEmp.isDelete === false - ) { + if (_profileEmp.keycloak != null && _profileEmp.keycloak != "" && _profileEmp.isDelete === false) { // disable keycloak/authen const delUserKeycloak = await deleteUser(_profileEmp.keycloak, adminToken); if (delUserKeycloak) { @@ -1899,21 +1886,6 @@ export class CommandController extends Controller { return new HttpSuccess(); } - /** - * API ทดสอบ cronjobPostRetireToExprofile - * @summary ทดสอบส่งข้อมูลผู้เกษียณไปยังระบบพ้นราชการ (Exprofile) - */ - @Get("cronjob/cronjobPostRetireToExprofile") - async runCronjobPostRetireToExprofile() { - try { - const retirementService = new RetirementService(); - const result = await retirementService.cronjobPostRetireToExprofile(); - return new HttpSuccess(result); - } catch (error: any) { - throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, error.message || "เกิดข้อผิดพลาด"); - } - } - /** * API รายละเอียดรายการคำสั่ง tab4 คำสั่ง * @@ -2723,22 +2695,23 @@ export class CommandController extends Controller { if (path == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ"); if (!["C-PM-26", "C-PM-25"].includes(commandCode)) { await new CallAPI() - .PostData(request, path, { - refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), - status: "REPORT", - }) - .then(async (res) => {}) - .catch(() => {}); - } else { + .PostData(request, path, { + refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), + status: "REPORT", + }) + .then(async (res) => {}) + .catch(() => {}); + } + else { await new CallAPI() - .PostData(request, path, { - refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), - status: "REPORT", - commandTypeId: requestBody.commandTypeId, - commandCode: commandCode, - }) - .then(async (res) => {}) - .catch(() => {}); + .PostData(request, path, { + refIds: requestBody.persons.filter((x) => x.refId != null).map((x) => x.refId), + status: "REPORT", + commandTypeId: requestBody.commandTypeId, + commandCode: commandCode, + }) + .then(async (res) => {}) + .catch(() => {}); } let order = command.commandRecives == null || command.commandRecives.length <= 0 @@ -3589,8 +3562,6 @@ export class CommandController extends Controller { positionLevel: string | null; posmasterId: string; positionId: string; - posExecutiveId?: string | null; - positionField?: string | null; commandId?: string | null; orgRoot?: string | null; orgChild1?: string | null; @@ -3687,51 +3658,11 @@ export class CommandController extends Controller { history.profileSalaryId = data.id; await this.salaryHistoryRepo.save(history, { data: req }); - // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา - let posMaster = await this.posMasterRepository.findOne({ + const posMaster = await this.posMasterRepository.findOne({ where: { id: item.posmasterId }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, }); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - } - - if (posMaster == null) { - console.error( - `[CommandController] PosMaster not found - posMasterId: ${item.posmasterId}, ` - ); + if (posMaster == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } const posMasterOld = await this.posMasterRepository.findOne({ where: { @@ -3757,7 +3688,7 @@ export class CommandController extends Controller { const checkPosition = await this.positionRepository.find({ where: { - posMasterId: posMaster.id, // ใช้ posMaster ตัวใหม่ (ที่อาจจะเปลี่ยนจาก ancestorDNA) + posMasterId: item.posmasterId, positionIsSelected: true, }, }); @@ -3779,121 +3710,19 @@ export class CommandController extends Controller { } await this.posMasterRepository.save(posMaster); - // STEP 2: กำหนด position ใหม่ - // Match position ตามลำดับ priority: - // Condition 1: match จาก positionId - // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) - // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) - // Fallback: เลือก position แรกใน posMaster - - let positionNew: Position | null = null; - - // ═══════════════════════════════════════════════════════════ - // CONDITION 1: เช็คจาก positionId ตรง - // ═══════════════════════════════════════════════════════════ - if (item.positionId) { - const positionById = await this.positionRepository.findOne({ - where: { - id: item.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionName && item.positionType && item.positionLevel) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: item.positionType, // positionType = posTypeId - posLevelId: item.positionLevel, // positionLevel = posLevelId - }; - - // เพิ่มเฉพาะฟิลด์ที่มีค่า (ไม่ใช่ null, undefined, หรือ string ว่าง) - if (item.positionField) { - whereCondition.positionField = item.positionField; - } - if (item.posExecutiveId) { - whereCondition.posExecutiveId = item.posExecutiveId; - } - if (item.positionExecutiveField) { - whereCondition.positionExecutiveField = item.positionExecutiveField; - } - if (item.positionArea) { - whereCondition.positionArea = item.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionName && item.positionType && item.positionLevel) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: item.positionType, // positionType = posTypeId - posLevelId: item.positionLevel, // positionLevel = posLevelId - }, - relations: ["posExecutive"], - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // ═══════════════════════════════════════════════════════════ - // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster - // // ═══════════════════════════════════════════════════════════ - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - + const positionNew = await this.positionRepository.findOne({ + where: { + id: item.positionId, + posMasterId: item.posmasterId, + }, + }); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - if (!posMaster.isSit) { + if(!posMaster.isSit){ profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; profile.position = positionNew.positionName; - profile.positionField = positionNew.positionField ?? null; - profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; - profile.positionArea = positionNew.positionArea ?? null; - profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; } profile.amount = item.amount ?? null; profile.amountSpecial = item.amountSpecial ?? null; @@ -3911,7 +3740,7 @@ export class CommandController extends Controller { public async newSalaryEmployeeAndUpdateCurrent( @Request() req: RequestWithUser, @Body() - body: { + body: { data: { profileId: string; amount?: Double | null; @@ -4119,8 +3948,6 @@ export class CommandController extends Controller { isLeave: boolean; leaveReason?: string | null; dateLeave?: Date | null; - posExecutiveId?: string | null; - positionField?: string | null; commandId?: string | null; isGovernment?: boolean | null; orgRoot?: string | null; @@ -4344,45 +4171,10 @@ export class CommandController extends Controller { //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ await removeProfileInOrganize(profile.id, "OFFICER"); //ปั๊มตำแหน่งใหม่ - // หา posMaster และเช็ค orgRevisionIsCurrent - let posMaster = await this.posMasterRepository.findOne({ + const posMaster = await this.posMasterRepository.findOne({ where: { id: item.posmasterId?.toString() }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, }); - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - } - if (posMaster) { const checkPosition = await this.positionRepository.find({ where: { @@ -4402,77 +4194,11 @@ export class CommandController extends Controller { // posMaster.conditionReason = _null; // posMaster.isCondition = false; await this.posMasterRepository.save(posMaster); - - // Match position ตามลำดับ priority - let positionNew: Position | null = null; - - // CONDITION 1: Match 7 ฟิลด์ (ไม่มี positionId ใน body สำหรับกรณีพักราชการ) - if (item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - const whereCondition: any = { + const positionNew = await this.positionRepository.findOne({ + where: { posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }; - - if (item.positionField) { - whereCondition.positionField = item.positionField; - } - if (item.posExecutiveId) { - whereCondition.posExecutiveId = item.posExecutiveId; - } - if (item.positionExecutiveField) { - whereCondition.positionExecutiveField = item.positionExecutiveField; - } - if (item.positionArea) { - whereCondition.positionArea = item.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - } - } - - // CONDITION 2: Match 3 ฟิลด์ (ถ้า Condition 1 ไม่ match) - if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }, - relations: ["posExecutive"], - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - + }, + }); if (positionNew) { positionNew.positionIsSelected = true; await this.positionRepository.save(positionNew, { data: req }); @@ -4611,6 +4337,20 @@ export class CommandController extends Controller { organizeName = names.join(" "); } + PostRetireToExprofile( + req, + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", + ); } }), ); @@ -4778,7 +4518,9 @@ export class CommandController extends Controller { profile.lastUpdateUserId = req.user.sub; profile.lastUpdateFullName = req.user.name; profile.lastUpdatedAt = new Date(); - // บันทึกประวัติก่อนลบตำแหน่ง + if (item.isLeave == true) { + await removeProfileInOrganize(profile.id, "EMPLOYEE"); + } const clearProfile = await checkCommandType(String(item.commandId)); const curRevision = await this.orgRevisionRepo.findOne({ where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, @@ -4807,14 +4549,6 @@ export class CommandController extends Controller { orgChild2Ref = curPosMaster?.orgChild2 ?? null; orgChild3Ref = curPosMaster?.orgChild3 ?? null; orgChild4Ref = curPosMaster?.orgChild4 ?? null; - if (curPosMaster) { - await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); - } - } - - // ลบตำแหน่ง - if (item.isLeave == true) { - await removeProfileInOrganize(profile.id, "EMPLOYEE"); } if (clearProfile.status) { @@ -4860,6 +4594,20 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } + PostRetireToExprofile( + req, + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + `${profile.posType?.posTypeShortName} ${profile.posLevel?.posLevelName}`, + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", + ); } }), ); @@ -5084,7 +4832,7 @@ export class CommandController extends Controller { agency: item.officerOrg, dateStart: item.dateStart, dateEnd: item.dateEnd, - commandNo: `${item.commandNo}/${_commandYear}`, + commandNo: `${item.commandNo}/${item.commandYear}`, commandName: item.commandName, refId: item.refId, refCommandDate: new Date(), @@ -5119,6 +4867,20 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } + PostRetireToExprofile( + req, + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", + ); } } }), @@ -5510,11 +5272,7 @@ export class CommandController extends Controller { const clearProfile = await checkCommandType(String(item.commandId)); if (clearProfile.status) { retireTypeName = clearProfile.retireTypeName ?? ""; - if ( - _profile.keycloak != null && - _profile.keycloak != "" && - _profile.isDelete === false - ) { + if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { const delUserKeycloak = await deleteUser(_profile.keycloak); if (delUserKeycloak) { // Task #228 @@ -5693,32 +5451,13 @@ export class CommandController extends Controller { _profile.leaveDate = item.commandDateAffect ?? _null; _profile.leaveType = exceptClear.LeaveType ?? _null; } else { - // บันทึกประวัติก่อนลบตำแหน่ง - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - if (curRevision) { - const curPosMaster = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: _profile.id, - orgRevisionId: curRevision.id, - }, - }); - if (curPosMaster) { - await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); - } - } await removeProfileInOrganize(_profile.id, "EMPLOYEE"); } } const clearProfile = await checkCommandType(String(item.commandId)); if (clearProfile.status) { retireTypeName = clearProfile.retireTypeName ?? ""; - if ( - _profile.keycloak != null && - _profile.keycloak != "" && - _profile.isDelete === false - ) { + if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { const delUserKeycloak = await deleteUser(_profile.keycloak); if (delUserKeycloak) { // Task #228 @@ -5763,6 +5502,21 @@ export class CommandController extends Controller { let _posLevelName: string = !isEmployee ? `${profile.posLevel?.posLevelName}` : `${profile.posType?.posTypeName} ${profile.posLevel?.posLevelName}`; + + PostRetireToExprofile( + req, + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + _posLevelName, + item.commandDateAffect ?? new Date(), + organizeName, + retireTypeName, + ); } }), ); @@ -6038,31 +5792,12 @@ export class CommandController extends Controller { _profile.leaveDate = item.commandDateAffect ?? _null; _profile.leaveType = exceptClear.LeaveType ?? _null; } else { - // บันทึกประวัติก่อนลบตำแหน่ง - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - if (curRevision) { - const curPosMaster = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: _profile.id, - orgRevisionId: curRevision.id, - }, - }); - if (curPosMaster) { - await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); - } - } await removeProfileInOrganize(_profile.id, "EMPLOYEE"); } } const clearProfile = await checkCommandType(String(item.commandId)); if (clearProfile.status) { - if ( - _profile.keycloak != null && - _profile.keycloak != "" && - _profile.isDelete === false - ) { + if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { const delUserKeycloak = await deleteUser(_profile.keycloak); if (delUserKeycloak) { // Task #228 @@ -6549,6 +6284,20 @@ export class CommandController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } + PostRetireToExprofile( + req, + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + item.commandDateAffect?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + item.commandDateAffect ?? new Date(), + organizeName, + clearProfile.retireTypeName ?? "", + ); }), ); return new HttpSuccess(); @@ -6567,13 +6316,6 @@ export class CommandController extends Controller { bodyPosition?: { posmasterId: string; positionId: string; - positionName: string; - posTypeId: string; - posLevelId: string; - posExecutiveId: string | null; - positionField: string | null; - positionExecutiveField: string | null; - positionArea: string | null; } | null; bodyMarry?: { marry?: boolean | null; @@ -6758,7 +6500,6 @@ export class CommandController extends Controller { relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], }); let _oldInsigniaIds: string[] = []; - let _oldSalaries: any[] = []; //ลูกจ้างประจำ หรือ บุคคลภายนอก if (!profile) { //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม @@ -6839,9 +6580,8 @@ export class CommandController extends Controller { profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; profile.isProbation = item.bodyProfile.isProbation; //เพิ่มใหม่จากรับโอน - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.prefix = item.bodyProfile.prefix ?? null; + profile.prefixMain = item.bodyProfile.prefix ?? null; profile.firstName = item.bodyProfile.firstName ?? null; profile.lastName = item.bodyProfile.lastName ?? null; profile.birthDate = item.bodyProfile.birthDate ?? null; @@ -6868,11 +6608,6 @@ export class CommandController extends Controller { profile.isLeave && ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) ) { - //ดึง profileSalary เดิม - _oldSalaries = await this.salaryRepo.find({ - where: { profileId: profile.id }, - order: { order: "ASC" }, - }); if (profile.profileInsignias.length > 0) { _oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? []; } @@ -6903,9 +6638,8 @@ export class CommandController extends Controller { profile.amount = item.bodyProfile.amount ?? null; profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; profile.isProbation = item.bodyProfile.isProbation; - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.prefix = item.bodyProfile.prefix ?? null; + profile.prefixMain = item.bodyProfile.prefix ?? null; profile.firstName = item.bodyProfile.firstName ?? null; profile.lastName = item.bodyProfile.lastName ?? null; profile.birthDate = item.bodyProfile.birthDate ?? null; @@ -6962,9 +6696,11 @@ export class CommandController extends Controller { profile.lastUpdateFullName = req.user.name; profile.lastUpdatedAt = new Date(); //เพิ่มใหม่จากรับโอน - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.prefix = + item.bodyProfile.prefix && item.bodyProfile.prefix != "" + ? item.bodyProfile.prefix + : profile.prefix; + profile.prefixMain = item.bodyProfile.prefix ?? null; profile.firstName = item.bodyProfile.firstName && item.bodyProfile.firstName != "" ? item.bodyProfile.firstName @@ -7110,23 +6846,6 @@ export class CommandController extends Controller { await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); } //Salary - //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ - if (_oldSalaries.length > 0) { - await Promise.all( - _oldSalaries.map(async (oldSal) => { - const profileSal: any = new ProfileSalary(); - Object.assign(profileSal, { ...oldSal, ...meta }); - const salaryHistory = new ProfileSalaryHistory(); - Object.assign(salaryHistory, { ...profileSal, id: undefined }); - profileSal.profileId = profile.id; - await this.salaryRepo.save(profileSal, { data: req }); - setLogDataDiff(req, { before, after: profileSal }); - salaryHistory.profileSalaryId = profileSal.id; - await this.salaryHistoryRepo.save(salaryHistory, { data: req }); - }), - ); - } - //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว if (item.bodySalarys && item.bodySalarys != null) { const dest_item = await this.salaryRepo.findOne({ where: { profileId: profile.id }, @@ -7157,20 +6876,12 @@ export class CommandController extends Controller { where: { id: item.bodyPosition.posmasterId, }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, + relations: { orgRevision: true } }); // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; + const isCurrent = posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA if (!isCurrent && posMaster?.ancestorDNA) { @@ -7179,26 +6890,15 @@ export class CommandController extends Controller { ancestorDNA: posMaster.ancestorDNA, orgRevision: { orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, + orgRevisionIsDraft: false + } }, + relations: { orgRevision: true } }); } - if (posMaster == null) { - console.error( - `[CreateOfficerProfile] not found posMasterId: ${item.bodyPosition.posmasterId}` - ); + if (posMaster == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน const posMasterOld = await this.posMasterRepository.findOne({ @@ -7252,128 +6952,56 @@ export class CommandController extends Controller { await this.posMasterRepository.save(posMaster); // STEP 5: กำหนด position ใหม่ - // Match position ตามลำดับ priority: - // Condition 1: match จาก positionId - // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) - // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) - // Fallback: เลือก position แรกใน posMaster + // เช็คว่า posMaster เปลี่ยนจากเก่าเป็นใหม่หรือไม่ + const originalPosMasterId = item.bodyPosition.posmasterId; + const isPosMasterChanged = originalPosMasterId !== posMaster.id; - let positionNew: Position | null = null; + let positionNew = null; - // ═══════════════════════════════════════════════════════════ - // CONDITION 1: เช็คจาก positionId ตรง - // ═══════════════════════════════════════════════════════════ - if (item.bodyPosition?.positionId) { - const positionById = await this.positionRepository.findOne({ + if (isPosMasterChanged) { + // posMaster เปลี่ยน ต้องหา position ใหม่จากคุณสมบัติของ position เก่า + // 1. หา position เก่าจาก id ที่ส่งมา + const positionOld = await this.positionRepository.findOne({ + where: { id: item.bodyPosition.positionId }, + }); + + if (positionOld) { + // 2. ใช้ posTypeId + posLevelId + positionName หา position ใหม่ใน posMaster ตัวใหม่ + positionNew = await this.positionRepository.findOne({ + where: { + posMasterId: posMaster.id, // ใช้ posMaster ตัวใหม่ + posTypeId: positionOld.posTypeId, + posLevelId: positionOld.posLevelId, + positionName: positionOld.positionName, + }, + }); + } + } else { + // posMaster ไม่เปลี่ยน - ใช้วิธีเดิม + positionNew = await this.positionRepository.findOne({ where: { id: item.bodyPosition.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่ไม่ใช่ null - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.bodyPosition.positionName, - posTypeId: item.bodyPosition.posTypeId, - posLevelId: item.bodyPosition.posLevelId, - }; - - if (item.bodyPosition.positionField) { - whereCondition.positionField = item.bodyPosition.positionField; - } - if (item.bodyPosition.posExecutiveId) { - whereCondition.posExecutiveId = item.bodyPosition.posExecutiveId; - } - if (item.bodyPosition.positionExecutiveField) { - whereCondition.positionExecutiveField = item.bodyPosition.positionExecutiveField; - } - if (item.bodyPosition.positionArea) { - whereCondition.positionArea = item.bodyPosition.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { posMasterId: posMaster.id, - positionName: item.bodyPosition.positionName, - posTypeId: item.bodyPosition.posTypeId, - posLevelId: item.bodyPosition.posLevelId, }, - relations: ["posExecutive"], }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } } - // // ═══════════════════════════════════════════════════════════ - // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster - // // ═══════════════════════════════════════════════════════════ - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { positionNew.positionIsSelected = true; - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - if (!posMaster.isSit) { + if(!posMaster.isSit){ profile.posLevelId = positionNew.posLevelId; profile.posTypeId = positionNew.posTypeId; profile.position = positionNew.positionName; - profile.positionField = positionNew.positionField ?? null; - profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; - profile.positionArea = positionNew.positionArea ?? null; - profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; // profile.dateStart = new Date(); + await this.profileRepository.save(profile, { data: req }); + setLogDataDiff(req, { before, after: profile }); } - await this.profileRepository.save(profile, { data: req }); - setLogDataDiff(req, { before, after: profile }); await this.positionRepository.save(positionNew, { data: req }); } // await CreatePosMasterHistoryOfficer(posMaster.id, req); await CreatePosMasterHistoryOfficer(posMaster.id, req, null, { - positionId: positionNew?.id, + positionId: positionNew?.id }); } // Insignia @@ -7972,8 +7600,6 @@ export class CommandController extends Controller { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบข้อมูล refIds"); } - const profileIdsToClearCache = new Set(); - await Promise.all( posMasters.map(async (item) => { // 4. ตรวจสอบข้อมูลที่จำเป็นทั้งหมด @@ -7982,10 +7608,6 @@ export class CommandController extends Controller { return; } - if (item.posMasterChild.current_holderId) { - profileIdsToClearCache.add(item.posMasterChild.current_holderId); - } - // 5. สร้าง orgShortName แบบปลอดภัย const orgShortName = [ @@ -8073,23 +7695,6 @@ export class CommandController extends Controller { }), ); - 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(); } diff --git a/src/controllers/CommandOperatorController.ts b/src/controllers/CommandOperatorController.ts index 18393f53..1a461ab3 100644 --- a/src/controllers/CommandOperatorController.ts +++ b/src/controllers/CommandOperatorController.ts @@ -9,7 +9,7 @@ import { Path, Request, Response, - Get, + Get } from "tsoa"; import { LessThan, MoreThan } from "typeorm"; import { AppDataSource } from "../database/data-source"; @@ -37,7 +37,9 @@ export class CommandOperatorController extends Controller { * @param commandId คีย์คำสั่ง */ @Get("{commandId}") - async getCommandOperatorByCommandId(@Path() commandId: string) { + async getCommandOperatorByCommandId( + @Path() commandId: string + ) { const command = await this.commandRepo.findOne({ where: { id: commandId }, select: { id: true }, @@ -59,7 +61,10 @@ export class CommandOperatorController extends Controller { * @param operatorId คีย์เจ้าหน้าที่ดำเนินการ */ @Get("swap/{direction}/{operatorId}") - async swapCommandOperator(@Path() direction: string, @Path() operatorId: string) { + async swapCommandOperator( + @Path() direction: string, + @Path() operatorId: string, + ) { const source = await this.commandOperatorRepo.findOne({ where: { id: operatorId }, }); @@ -101,7 +106,10 @@ export class CommandOperatorController extends Controller { source.orderNo = dest.orderNo; dest.orderNo = temp; - await Promise.all([this.commandOperatorRepo.save(source), this.commandOperatorRepo.save(dest)]); + await Promise.all([ + this.commandOperatorRepo.save(source), + this.commandOperatorRepo.save(dest), + ]); return new HttpSuccess(); } @@ -133,29 +141,35 @@ export class CommandOperatorController extends Controller { const nextOrderNo = (lastOrderNo?.orderNo ?? 1) + 1; const now = new Date(); - const operator = Object.assign(new CommandOperator(), { - ...body, - commandId: command.id, - orderNo: nextOrderNo, - createdUserId: request.user.sub, - createdFullName: request.user.name, - createdAt: now, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - lastUpdatedAt: now, - }); + const operator = Object.assign( + new CommandOperator(), + { + ...body, + commandId: command.id, + orderNo: nextOrderNo, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + } + ); await this.commandOperatorRepo.save(operator); return new HttpSuccess(); } - /** + /** * API ลบเจ้าหน้าที่ดำเนินการที่คำสั่ง * @summary API ลบเจ้าหน้าที่ดำเนินการที่คำสั่ง * @param commandId คีย์คำสั่ง * @param operatorId คีย์เจ้าหน้าที่ดำเนินการ */ @Delete("{commandId}/{operatorId}") - public async deleteCommandOperator(@Path() commandId: string, @Path() operatorId: string) { + public async deleteCommandOperator( + @Path() commandId: string, + @Path() operatorId: string, + ) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -201,9 +215,10 @@ export class CommandOperatorController extends Controller { return new HttpSuccess(true); } catch (error) { await queryRunner.rollbackTransaction(); - console.error("Delete command operator error:", error); + throw error; } finally { await queryRunner.release(); } } + } diff --git a/src/controllers/EmployeePositionController.ts b/src/controllers/EmployeePositionController.ts index 26146106..7b09973e 100644 --- a/src/controllers/EmployeePositionController.ts +++ b/src/controllers/EmployeePositionController.ts @@ -1058,11 +1058,11 @@ export class EmployeePositionController extends Controller { let checkChildConditions: any = {}; let keywordAsInt: any; let searchShortName = "1=1"; - let searchShortName0 = `CONCAT_WS(" ",orgRoot.orgRootShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName1 = `CONCAT_WS(" ",orgChild1.orgChild1ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName2 = `CONCAT_WS(" ",orgChild2.orgChild2ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName3 = `CONCAT_WS(" ",orgChild3.orgChild3ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName4 = `CONCAT_WS(" ",orgChild4.orgChild4ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; + let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`; + let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`; + let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`; + let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`; + let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`; let _data = await new permission().PermissionOrgList(request, "SYS_ORG_EMP"); if (body.type === 0) { typeCondition = { @@ -1072,7 +1072,7 @@ export class EmployeePositionController extends Controller { checkChildConditions = { orgChild1Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgRoot.orgRootShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } else { } } else if (body.type === 1) { @@ -1083,7 +1083,7 @@ export class EmployeePositionController extends Controller { checkChildConditions = { orgChild2Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgChild1.orgChild1ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } else { } } else if (body.type === 2) { @@ -1094,7 +1094,7 @@ export class EmployeePositionController extends Controller { checkChildConditions = { orgChild3Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgChild2.orgChild2ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } else { } } else if (body.type === 3) { @@ -1105,14 +1105,14 @@ export class EmployeePositionController extends Controller { checkChildConditions = { orgChild4Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgChild3.orgChild3ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } else { } } else if (body.type === 4) { typeCondition = { orgChild4Id: body.id, }; - searchShortName = `CONCAT_WS(" ",orgChild4.orgChild4ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } let findPosition: any; let masterId = new Array(); @@ -1190,8 +1190,8 @@ export class EmployeePositionController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `posMaster.orgChild1Id IN (:...child1)` - : // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `posMaster.orgChild1Id is null` + // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `posMaster.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -1226,24 +1226,23 @@ export class EmployeePositionController extends Controller { { child4: _data.child4, }, - ); + ) if (body.keyword != null && body.keyword != "") { - query - .orWhere( - new Brackets((qb) => { - qb.andWhere( - body.keyword != null && body.keyword != "" - ? body.isAll == false - ? searchShortName - : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` - : "1=1", - ) - .andWhere(checkChildConditions) - .andWhere(typeCondition) - .andWhere(revisionCondition); - }), - ) + query.orWhere( + new Brackets((qb) => { + qb.andWhere( + body.keyword != null && body.keyword != "" + ? body.isAll == false + ? searchShortName + : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` + : "1=1", + ) + .andWhere(checkChildConditions) + .andWhere(typeCondition) + .andWhere(revisionCondition); + }), + ) .orWhere( new Brackets((qb) => { qb.andWhere( @@ -1288,7 +1287,7 @@ export class EmployeePositionController extends Controller { .andWhere(typeCondition) .andWhere(revisionCondition); }), - ); + ) } let [posMaster, total] = await query @@ -1707,50 +1706,50 @@ export class EmployeePositionController extends Controller { const type0LastPosMasterNo = requestBody.type == 0 ? await this.employeePosMasterRepository.find({ - where: { - orgRootId: requestBody.id, - orgChild1Id: IsNull(), - }, - }) + where: { + orgRootId: requestBody.id, + orgChild1Id: IsNull(), + }, + }) : []; const type1LastPosMasterNo = requestBody.type == 1 ? await this.employeePosMasterRepository.find({ - where: { - orgChild1Id: requestBody.id, - orgChild2Id: IsNull(), - }, - }) + where: { + orgChild1Id: requestBody.id, + orgChild2Id: IsNull(), + }, + }) : []; const type2LastPosMasterNo = requestBody.type == 2 ? await this.employeePosMasterRepository.find({ - where: { - orgChild2Id: requestBody.id, - orgChild3Id: IsNull(), - }, - }) + where: { + orgChild2Id: requestBody.id, + orgChild3Id: IsNull(), + }, + }) : []; const type3LastPosMasterNo = requestBody.type == 3 ? await this.employeePosMasterRepository.find({ - where: { - orgChild3Id: requestBody.id, - orgChild4Id: IsNull(), - }, - }) + where: { + orgChild3Id: requestBody.id, + orgChild4Id: IsNull(), + }, + }) : []; const type4LastPosMasterNo = requestBody.type == 4 ? await this.employeePosMasterRepository.find({ - where: { - orgChild4Id: requestBody.id, - }, - }) + where: { + orgChild4Id: requestBody.id, + }, + }) : []; const allLastPosMasterNo = [ @@ -2414,7 +2413,7 @@ export class EmployeePositionController extends Controller { */ @Post("profile/delete/{id}") 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({ where: { id: id }, relations: ["positions", "orgRevision"], @@ -2473,7 +2472,7 @@ export class EmployeePositionController extends Controller { @Body() requestBody: { draftPositionId: string; publishPositionId: string }, @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({ where: { orgRevisionIsDraft: true, diff --git a/src/controllers/EmployeeTempPositionController.ts b/src/controllers/EmployeeTempPositionController.ts index e5229e67..69dc3b92 100644 --- a/src/controllers/EmployeeTempPositionController.ts +++ b/src/controllers/EmployeeTempPositionController.ts @@ -908,8 +908,8 @@ export class EmployeeTempPositionController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `posMaster.orgChild1Id IN (:...child1)` - : // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `posMaster.orgChild1Id is null` + // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `posMaster.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -944,24 +944,23 @@ export class EmployeeTempPositionController extends Controller { { child4: _data.child4, }, - ); + ) if (body.keyword != null && body.keyword != "") { - query - .orWhere( - new Brackets((qb) => { - qb.andWhere( - body.keyword != null && body.keyword != "" - ? body.isAll == false - ? searchShortName - : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` - : "1=1", - ) - .andWhere(checkChildConditions) - .andWhere(typeCondition) - .andWhere(revisionCondition); - }), - ) + query.orWhere( + new Brackets((qb) => { + qb.andWhere( + body.keyword != null && body.keyword != "" + ? body.isAll == false + ? searchShortName + : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` + : "1=1", + ) + .andWhere(checkChildConditions) + .andWhere(typeCondition) + .andWhere(revisionCondition); + }), + ) .orWhere( new Brackets((qb) => { qb.andWhere( @@ -1006,7 +1005,7 @@ export class EmployeeTempPositionController extends Controller { .andWhere(typeCondition) .andWhere(revisionCondition); }), - ); + ) } let [posMaster, total] = await query @@ -1422,50 +1421,50 @@ export class EmployeeTempPositionController extends Controller { const type0LastPosMasterNo = requestBody.type == 0 ? await this.employeeTempPosMasterRepository.find({ - where: { - orgRootId: requestBody.id, - orgChild1Id: IsNull(), - }, - }) + where: { + orgRootId: requestBody.id, + orgChild1Id: IsNull(), + }, + }) : []; const type1LastPosMasterNo = requestBody.type == 1 ? await this.employeeTempPosMasterRepository.find({ - where: { - orgChild1Id: requestBody.id, - orgChild2Id: IsNull(), - }, - }) + where: { + orgChild1Id: requestBody.id, + orgChild2Id: IsNull(), + }, + }) : []; const type2LastPosMasterNo = requestBody.type == 2 ? await this.employeeTempPosMasterRepository.find({ - where: { - orgChild2Id: requestBody.id, - orgChild3Id: IsNull(), - }, - }) + where: { + orgChild2Id: requestBody.id, + orgChild3Id: IsNull(), + }, + }) : []; const type3LastPosMasterNo = requestBody.type == 3 ? await this.employeeTempPosMasterRepository.find({ - where: { - orgChild3Id: requestBody.id, - orgChild4Id: IsNull(), - }, - }) + where: { + orgChild3Id: requestBody.id, + orgChild4Id: IsNull(), + }, + }) : []; const type4LastPosMasterNo = requestBody.type == 4 ? await this.employeeTempPosMasterRepository.find({ - where: { - orgChild4Id: requestBody.id, - }, - }) + where: { + orgChild4Id: requestBody.id, + }, + }) : []; const allLastPosMasterNo = [ @@ -2119,7 +2118,7 @@ export class EmployeeTempPositionController extends Controller { */ @Post("profile/delete/{id}") 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({ where: { id: id }, relations: ["positions", "orgRevision"], @@ -2180,7 +2179,7 @@ export class EmployeeTempPositionController extends Controller { @Body() requestBody: { draftPositionId: string; publishPositionId: string }, @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({ where: { orgRevisionIsDraft: true, diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index 6720f19d..128cb4d1 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -15,7 +15,6 @@ import { import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { addLogSequence } from "../interfaces/utils"; -import HttpSuccess from "../interfaces/http-success"; interface CachedToken { token: string; @@ -89,8 +88,7 @@ export class ExRetirementController extends Controller { }, }); - // return res.data; - return new HttpSuccess(res.data.data); + return res.data; } catch (error: any) { if (error.response?.status === 500 && retryCount < maxRetries - 1) { TokenCache.delete(`${clientId}:${clientSecret}`); @@ -237,19 +235,16 @@ export async function PostRetireToExprofile( 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), - }, - }); - } + 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 ได้"); } diff --git a/src/controllers/ImportDataController.ts b/src/controllers/ImportDataController.ts index b424edbb..5b8ca808 100644 --- a/src/controllers/ImportDataController.ts +++ b/src/controllers/ImportDataController.ts @@ -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 { In, IsNull, LessThanOrEqual, Not, Between } from "typeorm"; import HttpSuccess from "../interfaces/http-success"; @@ -105,7 +105,6 @@ import { positionOfficer } from "../entities/mis/positionOfficer"; import { ProvinceMaster } from "../entities/ProvinceMaster"; import { SubDistrictMaster } from "../entities/SubDistrictMaster"; import { DistrictMaster } from "../entities/DistrictMaster"; -import { RequestWithUser } from "../middlewares/user"; @Route("api/v1/org/upload") @Tags("UPLOAD") @Security("bearerAuth") @@ -6816,523 +6815,4 @@ export class ImportDataController extends Controller { // await repo.save(entities); // return entities; // } - - /** - * @summary Import ข้อมูลประวัติตำแหน่งเงินเดือนของข้าราชการเข้าตาราง ProfileSalaryTemp - * @param profileId Id โปรไฟล์ข้าราชการ - * @param file Excel file with salary history data - */ - @Post("office-profileSalaryTemp/{profileId}") - @UseInterceptors(FileInterceptor("file")) - async UploadProfileSalaryTemp( - @Path() profileId: string, - @Request() req: RequestWithUser, - @UploadedFile() file: Express.Multer.File, - ) { - if (!profileId) { - throw new Error("profileId is required"); - } - - // อ่านไฟล์ Excel ก่อน (นอก transaction) - const workbook = xlsx.read(file.buffer, { type: "buffer" }); - const sheetName = workbook.SheetNames[0]; - const sheet = workbook.Sheets[sheetName]; - const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][]; - - let salaryTemps: ProfileSalaryTemp[] = []; - let dateTime = new Date(); - - // เริ่มจาก index 1 เพื่อข้าม header row - for (let i = 1; i < getExcel.length; i++) { - const row = getExcel[i]; - - // ข้าม empty rows - if (!row || row.length === 0) { - continue; - } - - // ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง) - if (!row[0]) { - continue; - } - - const salaryTemp = new ProfileSalaryTemp(); - - // ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number - const parseExcelDate = (value: any): Date | null => { - if (!value) return null; - - // กรณี 1: Excel serial number (ตัวเลข) - if (typeof value === "number") { - // Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900 - // แปลงเป็น JavaScript Date (epoch 1970) - let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000)); - - // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) - if (jsDate.getFullYear() > 2500) { - const newYear = jsDate.getFullYear() - 543; - jsDate = new Date( - newYear, - jsDate.getMonth(), - jsDate.getDate(), - jsDate.getHours(), - jsDate.getMinutes(), - jsDate.getSeconds(), - jsDate.getMilliseconds(), - ); - } - return jsDate; - } - - // กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy) - const dateStr = value.toString().trim(); - - // ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่ - if (/^\d+$/.test(dateStr)) { - const serialNum = parseInt(dateStr); - let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000)); - - // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) - if (jsDate.getFullYear() > 2500) { - const newYear = jsDate.getFullYear() - 543; - jsDate = new Date( - newYear, - jsDate.getMonth(), - jsDate.getDate(), - jsDate.getHours(), - jsDate.getMinutes(), - jsDate.getSeconds(), - jsDate.getMilliseconds(), - ); - } - return jsDate; - } - - // String format ปกติ (dd/mm/yyyy) - const dateParts = dateStr.split("/"); - if (dateParts.length === 3) { - // แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก - const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0"); - const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0"); - let year = parseInt(dateParts[2].trim()); - if (year > 2500) { - year -= 543; - } - const result = new Date(`${year}-${month}-${day}`); - return result; - } - - return null; - }; - - // Index 1: วันที่คำสั่งมีผล - let commandDateAffect: Date | null = null; - if (row[1]) { - commandDateAffect = parseExcelDate(row[1]); - } - - // Index 25: วันที่ลงนาม - let commandDateSign: Date | null = null; - if (row[25]) { - commandDateSign = parseExcelDate(row[25]); - } - - // Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column - // ข้อมูลระบบ - salaryTemp.profileId = profileId; - salaryTemp.profileEmployeeId = null as any; - - // Index 0: ลำดับ - salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any); - - // Index 1: วันที่คำสั่งมีผล - salaryTemp.commandDateAffect = commandDateAffect as any; - - // Index 2: ตำแหน่งในสายงาน - salaryTemp.positionName = row[2] || null; - - // Index 3: ตำแหน่งประเภท - salaryTemp.positionType = row[3] || null; - - // Index 4: ระดับ - salaryTemp.positionLevel = row[4] || null; - - // Index 5: ระดับซี - salaryTemp.positionCee = row[5] || null; - - // Index 6: สายงาน - salaryTemp.positionLine = row[6] || null; - - // Index 7: ด้าน/สาขา - salaryTemp.positionPathSide = row[7] || null; - - // Index 8: ตำแหน่งทางการบริหาร - salaryTemp.positionExecutive = row[8] || null; - - // Index 9: ด้านทางการบริหาร - salaryTemp.positionExecutiveField = row[9] || null; - - // Index 10: เงินเดือน - salaryTemp.amount = row[10] || 0; - - // Index 11: เงินค่าตอบแทนรายเดือน - salaryTemp.mouthSalaryAmount = row[11] || 0; - - // Index 12: เงินประจำตำแหน่ง - salaryTemp.positionSalaryAmount = row[12] || 0; - - // Index 13: เงินค่าตอบแทนพิเศษ - salaryTemp.amountSpecial = row[13] || 0; - - // Index 14: หน่วยงาน - salaryTemp.orgRoot = row[14] || null; - - // Index 15: ส่วนราชการระดับ 1 - salaryTemp.orgChild1 = row[15] || null; - - // Index 16: ส่วนราชการระดับ 2 - salaryTemp.orgChild2 = row[16] || null; - - // Index 17: ส่วนราชการระดับ 3 - salaryTemp.orgChild3 = row[17] || null; - - // Index 18: ส่วนราชการระดับ 4 - salaryTemp.orgChild4 = row[18] || null; - - // Index 19: ตัวย่อเลขที่ตำแหน่ง - salaryTemp.posNoAbb = row[19] || null; - - // Index 20: เลขที่ตำแหน่ง - salaryTemp.posNo = row[20] ? row[20].toString() : null; - - // Index 21: หน่วยงานที่ออกคำสั่ง - salaryTemp.posNumCodeSit = row[21] || null; - - // Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง - salaryTemp.posNumCodeSitAbb = row[22] || null; - - // Index 23: เลขที่คำสั่ง - salaryTemp.commandNo = row[23] || null; - - // Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.) - let commandYearValue: number | null = null; - if (row[24]) { - commandYearValue = parseInt(row[24].toString()); - // ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ. - if (commandYearValue > 2500) { - commandYearValue -= 543; - } - } - salaryTemp.commandYear = commandYearValue as any; - - // Index 25: วันที่ลงนาม (แปลงแล้ว) - salaryTemp.commandDateSign = commandDateSign as any; - - // Index 26: ประเภทคำสั่ง - salaryTemp.commandName = row[26] || null; - - // Index 27: หมายเหตุ - salaryTemp.remark = row[27] || null; - - // Index 28: commandId - salaryTemp.commandId = row[28] || null; - - // Index 29: commandCode - salaryTemp.commandCode = row[29] || null; - - // ข้อมูลระบบ - salaryTemp.isDelete = false; - salaryTemp.isEdit = false; - salaryTemp.isGovernment = false; - salaryTemp.isEntry = false; - salaryTemp.createdAt = dateTime; - salaryTemp.createdUserId = req.user?.sub || ""; - salaryTemp.createdFullName = req.user?.name || "System Administrator"; - salaryTemp.lastUpdatedAt = dateTime; - salaryTemp.lastUpdateUserId = req.user?.sub || ""; - salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator"; - - // 12,15,16 isGovernment = false & dateGovernment = salaryTemp.commandDateAffect - if (["12", "15", "16"].includes(salaryTemp.commandCode ?? "")) { - salaryTemp.isGovernment = false; - salaryTemp.dateGovernment = salaryTemp.commandDateAffect; - } - // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = salaryTemp.commandDateAffect - else if (["1", "2", "3", "4", "10", "11", "20"].includes(salaryTemp.commandCode ?? "")) { - salaryTemp.isGovernment = true; - salaryTemp.dateGovernment = salaryTemp.commandDateAffect; - } - - salaryTemps.push(salaryTemp); - } - - // ใช้ Transaction เพื่อความปลอดภัย - await AppDataSource.transaction(async (transactionalEntityManager) => { - // ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileId นั้น - await transactionalEntityManager.delete(ProfileSalaryTemp, { profileId }); - // Insert ข้อมูลใหม่ - await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps); - }); - - return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length }); - } - - /** - * @summary Import ข้อมูลประวัติตำแหน่งเงินเดือนของลูกจ้างประจำเข้าตาราง ProfileSalaryTemp - * @param profileEmployeeId Id โปรไฟล์ลูกจ้างประจำ - * @param file Excel file with salary history data - */ - @Post("employee-profileSalaryTemp/{profileEmployeeId}") - @UseInterceptors(FileInterceptor("file")) - async UploadProfileEmployeeSalaryTemp( - @Path() profileEmployeeId: string, - @Request() req: RequestWithUser, - @UploadedFile() file: Express.Multer.File, - ) { - if (!profileEmployeeId) { - throw new Error("profileEmployeeId is required"); - } - - // อ่านไฟล์ Excel ก่อน (นอก transaction) - const workbook = xlsx.read(file.buffer, { type: "buffer" }); - const sheetName = workbook.SheetNames[0]; - const sheet = workbook.Sheets[sheetName]; - const getExcel = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][]; - - let salaryTemps: ProfileSalaryTemp[] = []; - let dateTime = new Date(); - - // เริ่มจาก index 1 เพื่อข้าม header row - for (let i = 1; i < getExcel.length; i++) { - const row = getExcel[i]; - - // ข้าม empty rows - if (!row || row.length === 0) { - continue; - } - - // ข้ามแถวที่ไม่มีลำดับ (row[0] เป็น null, undefined หรือค่าว่าง) - if (!row[0]) { - continue; - } - - const salaryTemp = new ProfileSalaryTemp(); - - // ฟังก์ชันแปลงวันที่จาก Excel รองรับทั้ง string format และ serial number - const parseExcelDate = (value: any): Date | null => { - if (!value) return null; - - // กรณี 1: Excel serial number (ตัวเลข) - if (typeof value === "number") { - // Excel serial number = จำนวนวันตั้งแต่ 1 ม.ค. 1900 - // แปลงเป็น JavaScript Date (epoch 1970) - let jsDate = new Date(Math.round((value - 25569) * 86400 * 1000)); - - // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) - if (jsDate.getFullYear() > 2500) { - const newYear = jsDate.getFullYear() - 543; - jsDate = new Date( - newYear, - jsDate.getMonth(), - jsDate.getDate(), - jsDate.getHours(), - jsDate.getMinutes(), - jsDate.getSeconds(), - jsDate.getMilliseconds(), - ); - } - return jsDate; - } - - // กรณี 2: String format (dd/mm/yyyy หรือ d/m/yyyy) - const dateStr = value.toString().trim(); - - // ตรวจสอบว่าเป็น serial number ที่เป็น string หรือไม่ - if (/^\d+$/.test(dateStr)) { - const serialNum = parseInt(dateStr); - let jsDate = new Date(Math.round((serialNum - 25569) * 86400 * 1000)); - - // ตรวจสอบและแปลงปี พ.ศ. เป็น ค.ศ. (ถ้าปี > 2500) - if (jsDate.getFullYear() > 2500) { - const newYear = jsDate.getFullYear() - 543; - jsDate = new Date( - newYear, - jsDate.getMonth(), - jsDate.getDate(), - jsDate.getHours(), - jsDate.getMinutes(), - jsDate.getSeconds(), - jsDate.getMilliseconds(), - ); - } - return jsDate; - } - - // String format ปกติ (dd/mm/yyyy) - const dateParts = dateStr.split("/"); - if (dateParts.length === 3) { - // แปลงเป็นตัวเลขแล้วค่อยจัดรูปแบบใหม่ เพื่อรองรับทั้ง 1 หลักและ 2 หลัก - const day = parseInt(dateParts[0].trim()).toString().padStart(2, "0"); - const month = parseInt(dateParts[1].trim()).toString().padStart(2, "0"); - let year = parseInt(dateParts[2].trim()); - if (year > 2500) { - year -= 543; - } - const result = new Date(`${year}-${month}-${day}`); - return result; - } - - return null; - }; - - // Index 1: วันที่คำสั่งมีผล - let commandDateAffect: Date | null = null; - if (row[1]) { - commandDateAffect = parseExcelDate(row[1]); - } - - // Index 25: วันที่ลงนาม - let commandDateSign: Date | null = null; - if (row[25]) { - commandDateSign = parseExcelDate(row[25]); - } - - // Map ข้อมูลจาก Excel ไปยัง ProfileSalaryTemp ตามลำดับ column - // ข้อมูลระบบ - salaryTemp.profileEmployeeId = profileEmployeeId; - salaryTemp.profileId = null as any; - - // Index 0: ลำดับ - salaryTemp.order = row[0] ? parseInt(row[0].toString()) : (null as any); - - // Index 1: วันที่คำสั่งมีผล - salaryTemp.commandDateAffect = commandDateAffect as any; - - // Index 2: ตำแหน่งในสายงาน - salaryTemp.positionName = row[2] || null; - - // Index 3: ตำแหน่งประเภท - salaryTemp.positionType = row[3] || null; - - // Index 4: ระดับ - salaryTemp.positionLevel = row[4] || null; - - // Index 5: ระดับซี - salaryTemp.positionCee = row[5] || null; - - // Index 6: สายงาน - salaryTemp.positionLine = row[6] || null; - - // Index 7: ด้าน/สาขา - salaryTemp.positionPathSide = row[7] || null; - - // Index 8: ตำแหน่งทางการบริหาร - salaryTemp.positionExecutive = row[8] || null; - - // Index 9: ด้านทางการบริหาร - salaryTemp.positionExecutiveField = row[9] || null; - - // Index 10: เงินเดือน - salaryTemp.amount = row[10] || 0; - - // Index 11: เงินค่าตอบแทนรายเดือน - salaryTemp.mouthSalaryAmount = row[11] || 0; - - // Index 12: เงินประจำตำแหน่ง - salaryTemp.positionSalaryAmount = row[12] || 0; - - // Index 13: เงินค่าตอบแทนพิเศษ - salaryTemp.amountSpecial = row[13] || 0; - - // Index 14: หน่วยงาน - salaryTemp.orgRoot = row[14] || null; - - // Index 15: ส่วนราชการระดับ 1 - salaryTemp.orgChild1 = row[15] || null; - - // Index 16: ส่วนราชการระดับ 2 - salaryTemp.orgChild2 = row[16] || null; - - // Index 17: ส่วนราชการระดับ 3 - salaryTemp.orgChild3 = row[17] || null; - - // Index 18: ส่วนราชการระดับ 4 - salaryTemp.orgChild4 = row[18] || null; - - // Index 19: ตัวย่อเลขที่ตำแหน่ง - salaryTemp.posNoAbb = row[19] || null; - - // Index 20: เลขที่ตำแหน่ง - salaryTemp.posNo = row[20] ? row[20].toString() : null; - - // Index 21: หน่วยงานที่ออกคำสั่ง - salaryTemp.posNumCodeSit = row[21] || null; - - // Index 22: ตัวย่อหน่วยงานที่ออกคำสั่ง - salaryTemp.posNumCodeSitAbb = row[22] || null; - - // Index 23: เลขที่คำสั่ง - salaryTemp.commandNo = row[23] || null; - - // Index 24: ปีเลขที่คำสั่ง (แปลงเป็น ค.ศ.) - let commandYearValue: number | null = null; - if (row[24]) { - commandYearValue = parseInt(row[24].toString()); - // ถ้าปีเป็น พ.ศ. (มากกว่า 2500) ให้แปลงเป็น ค.ศ. - if (commandYearValue > 2500) { - commandYearValue -= 543; - } - } - salaryTemp.commandYear = commandYearValue as any; - - // Index 25: วันที่ลงนาม (แปลงแล้ว) - salaryTemp.commandDateSign = commandDateSign as any; - - // Index 26: ประเภทคำสั่ง - salaryTemp.commandName = row[26] || null; - - // Index 27: หมายเหตุ - salaryTemp.remark = row[27] || null; - - // Index 28: commandId - salaryTemp.commandId = row[28] || null; - - // Index 29: commandCode - salaryTemp.commandCode = row[29] || null; - - // ข้อมูลระบบ - salaryTemp.isDelete = false; - salaryTemp.isEdit = false; - salaryTemp.isGovernment = false; - salaryTemp.isEntry = false; - salaryTemp.createdAt = dateTime; - salaryTemp.createdUserId = req.user?.sub || ""; - salaryTemp.createdFullName = req.user?.name || "System Administrator"; - salaryTemp.lastUpdatedAt = dateTime; - salaryTemp.lastUpdateUserId = req.user?.sub || ""; - salaryTemp.lastUpdateFullName = req.user?.name || "System Administrator"; - - // 12,15,16 isGovernment = false & dateGovernment = salaryTemp.commandDateAffect - if (["12", "15", "16"].includes(salaryTemp.commandCode ?? "")) { - salaryTemp.isGovernment = false; - salaryTemp.dateGovernment = salaryTemp.commandDateAffect; - } - // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = salaryTemp.commandDateAffect - else if (["1", "2", "3", "4", "10", "11", "20"].includes(salaryTemp.commandCode ?? "")) { - salaryTemp.isGovernment = true; - salaryTemp.dateGovernment = salaryTemp.commandDateAffect; - } - salaryTemps.push(salaryTemp); - } - - // ใช้ Transaction เพื่อความปลอดภัย - await AppDataSource.transaction(async (transactionalEntityManager) => { - // ล้างข้อมูลทั้งหมดในตาราง profileSalaryTemp ของ profileEmployeeId นั้น - await transactionalEntityManager.delete(ProfileSalaryTemp, { profileEmployeeId }); - // Insert ข้อมูลใหม่ - await transactionalEntityManager.save(ProfileSalaryTemp, salaryTemps); - }); - - return new HttpSuccess({ message: "Import ข้อมูลเรียบร้อย", count: salaryTemps.length }); - } } diff --git a/src/controllers/KeycloakSyncController.ts b/src/controllers/KeycloakSyncController.ts index 0a749732..995fa3c0 100644 --- a/src/controllers/KeycloakSyncController.ts +++ b/src/controllers/KeycloakSyncController.ts @@ -315,81 +315,4 @@ export class KeycloakSyncController extends Controller { ...result, }); } - - /** - * Sync profiles with missing empType for a specific month (Admin only) - * - * @summary Find profiles updated in specified month with missing empType in Keycloak and sync them (ADMIN) - * - * @description - * This endpoint will: - * - List profiles from Profile table where lastUpdatedAt falls within the specified month - * - For each profile, check Keycloak if empType attribute is empty/null - * - If empType is empty, sync the profile using existing sync logic - * - Return summary of sync results - * - * Features: - * - Dry run mode (dryRun=true) to check without syncing - * - Configurable concurrency for parallel processing - * - Rate limiting to avoid overwhelming Keycloak - * - Detailed error reporting - * - Idempotent (can be safely re-run) - * - * @param {request} request Request body containing month parameter - * @param dryRun - If true, only check without syncing (default: false) - * @param concurrency - Number of parallel operations (default: 5) - * @param rateLimit - Requests per second limit (default: 10) - */ - @Post("sync-missing-emptype") - @Response(HttpStatus.BAD_REQUEST, "Invalid month format") - @Response(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, - }); - } } diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index e107df5c..39752b7e 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -61,17 +61,12 @@ import { BatchSavePosMasterHistoryOfficer, CreatePosMasterHistoryEmployee, CreatePosMasterHistoryOfficer, + SavePosMasterHistoryOfficer, } from "../services/PositionService"; import { orgStructureCache } from "../utils/OrgStructureCache"; import { OrgIdMapping, AllOrgMappings, SavePosMasterHistory } from "../interfaces/OrgMapping"; import { OrgPermissionData, NodeLevel } from "../interfaces/OrgTypes"; -import { - formatPosMaster, - generateLabelName, - filterPosMasters, - getPosMasterNo, - getOrgFullName, -} from "../utils/org-formatting"; +import { formatPosMaster, generateLabelName, filterPosMasters } from "../utils/org-formatting"; @Route("api/v1/org") @Tags("Organization") @@ -214,7 +209,6 @@ export class OrganizationController extends Controller { await sendToQueueOrgDraft(msg); return new HttpSuccess("Draft is being created... Processing in the background."); } catch (error: any) { - console.error("Error creating draft organization:", error); throw error; } } @@ -2530,7 +2524,6 @@ export class OrganizationController extends Controller { await sendToQueueOrg(msg); return new HttpSuccess(); } catch (error: any) { - console.error("Error publishing draft organization:", error); throw error; } } @@ -2539,18 +2532,11 @@ export class OrganizationController extends Controller { * Cronjob */ async cronjobRevision() { - console.log("[CronJob] cronjobRevision START"); - const startTime = Date.now(); - const today = new Date(); today.setUTCHours(0, 0, 0, 0); // Set time to the beginning of the day const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); - console.log( - `[CronJob] Searching for draft revision with publishDate between ${today.toISOString()} and ${tomorrow.toISOString()}`, - ); - const orgRevisionDraft = await this.orgRevisionRepository .createQueryBuilder("orgRevision") .where("orgRevision.orgRevisionIsDraft = true") @@ -2559,14 +2545,8 @@ export class OrganizationController extends Controller { .getOne(); if (!orgRevisionDraft) { - console.log("[CronJob] No draft revision found to publish"); return new HttpSuccess(); } - - console.log( - `[CronJob] Found draft revision: ${orgRevisionDraft.id}, name: ${orgRevisionDraft.orgRevisionName}, publishDate: ${orgRevisionDraft.orgPublishDate}`, - ); - // if (orgRevisionPublish) { // orgRevisionPublish.orgRevisionIsDraft = false; // orgRevisionPublish.orgRevisionIsCurrent = false; @@ -2595,10 +2575,7 @@ export class OrganizationController extends Controller { lastUpdatedAt: new Date(), }, }; - - console.log(`[CronJob] Sending to RabbitMQ queue - revisionId: ${orgRevisionDraft.id}`); sendToQueueOrg(msg); - console.log(`[CronJob] Sent to queue successfully - Total time: ${Date.now() - startTime}ms`); return new HttpSuccess(); } @@ -5809,7 +5786,6 @@ export class OrganizationController extends Controller { .leftJoin("orgRoot.posMasters", "posMasters") .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgRoot.orgRootOrder", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") .getMany(); const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id) || null; @@ -5850,7 +5826,6 @@ export class OrganizationController extends Controller { .leftJoin("orgChild1.posMasters", "posMasters") .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild1.orgChild1Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") .getMany() : []; @@ -5892,7 +5867,6 @@ export class OrganizationController extends Controller { .leftJoin("orgChild2.posMasters", "posMasters") .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild2.orgChild2Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") .getMany() : []; @@ -5934,7 +5908,6 @@ export class OrganizationController extends Controller { .leftJoin("orgChild3.posMasters", "posMasters") .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild3.orgChild3Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") .getMany() : []; @@ -5971,7 +5944,6 @@ export class OrganizationController extends Controller { .leftJoin("orgChild4.posMasters", "posMasters") .leftJoin("posMasters.current_holder", "current_holder") .orderBy("orgChild4.orgChild4Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") .getMany() : []; @@ -7853,11 +7825,7 @@ export class OrganizationController extends Controller { profileEmp.lastUpdatedAt = new Date(); profileEmp.isActive = false; - if ( - profileEmp.keycloak != null && - profileEmp.keycloak != "" && - profileEmp.isDelete === false - ) { + if (profileEmp.keycloak != null && profileEmp.keycloak != "" && profileEmp.isDelete === false) { const delUserKeycloak = await deleteUser(profileEmp.keycloak, token); if (delUserKeycloak) { // profileEmp.keycloak = ""; @@ -8155,8 +8123,6 @@ export class OrganizationController extends Controller { if (!orgRootDraft) return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้างร่าง"); - let createdCurrentRoot = false; - // if current record not found, create new one if (!orgRootCurrent) { // Create new current record using draft's ID @@ -8168,7 +8134,6 @@ export class OrganizationController extends Controller { const savedRoot = await queryRunner.manager.save(OrgRoot, newCurrentRoot); orgRootCurrent = savedRoot; // Use saved record for sync - createdCurrentRoot = true; } // Part 1: Differential sync of organization structure (bottom-up) @@ -8194,7 +8159,11 @@ export class OrganizationController extends Controller { mapping: OrgIdMapping; counts: { deleted: number; updated: number; inserted: number }; }; - if (createdCurrentRoot && orgRootCurrent && orgRootDraft) { + if ( + orgRootCurrent && + orgRootDraft && + orgRootCurrent.ancestorDNA === orgRootDraft.ancestorDNA + ) { // Manually created - set up mapping directly const rootMapping: OrgIdMapping = { byAncestorDNA: new Map([[orgRootDraft.ancestorDNA, orgRootCurrent.id]]), @@ -8212,7 +8181,6 @@ export class OrganizationController extends Controller { this.orgRootRepository, drafRevisionId, currentRevisionId, - rootDnaId, allMappings, orgRootDraft?.id, orgRootCurrent?.id, @@ -8228,7 +8196,6 @@ export class OrganizationController extends Controller { this.child1Repository, drafRevisionId, currentRevisionId, - rootDnaId, allMappings, orgRootDraft?.id, orgRootCurrent?.id, @@ -8243,7 +8210,6 @@ export class OrganizationController extends Controller { this.child2Repository, drafRevisionId, currentRevisionId, - rootDnaId, allMappings, orgRootDraft?.id, orgRootCurrent?.id, @@ -8258,7 +8224,6 @@ export class OrganizationController extends Controller { this.child3Repository, drafRevisionId, currentRevisionId, - rootDnaId, allMappings, orgRootDraft?.id, orgRootCurrent?.id, @@ -8273,7 +8238,6 @@ export class OrganizationController extends Controller { this.child4Repository, drafRevisionId, currentRevisionId, - rootDnaId, allMappings, orgRootDraft?.id, orgRootCurrent?.id, @@ -8316,7 +8280,6 @@ export class OrganizationController extends Controller { if (posMasterDraft.length <= 0) { // Fetch current positions const posMasterCurrent = await this.posMasterRepository.find({ - relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], where: [ { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, @@ -8339,34 +8302,7 @@ export class OrganizationController extends Controller { const deleteHistoryOps = posMasterCurrent.map((pos) => ({ posMasterDnaId: pos.ancestorDNA, profileId: null, - pm: { - prefix: null, - firstName: null, - lastName: null, - position: null, - posType: null, - posLevel: null, - posExecutive: null, - profileId: null, - shortName: pos - ? [ - pos.orgChild4?.orgChild4ShortName, - pos.orgChild3?.orgChild3ShortName, - pos.orgChild2?.orgChild2ShortName, - pos.orgChild1?.orgChild1ShortName, - pos.orgRoot?.orgRootShortName, - ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? - null - : null, - posMasterNoPrefix: pos.posMasterNoPrefix ?? null, - posMasterNo: pos.posMasterNo != null ? String(pos.posMasterNo) : null, - posMasterNoSuffix: pos.posMasterNoSuffix ?? null, - rootDnaId: pos?.orgRoot?.ancestorDNA ?? null, - child1DnaId: pos?.orgChild1?.ancestorDNA ?? null, - child2DnaId: pos?.orgChild2?.ancestorDNA ?? null, - child3DnaId: pos?.orgChild3?.ancestorDNA ?? null, - child4DnaId: pos?.orgChild4?.ancestorDNA ?? null, - } as SavePosMasterHistory, + pm: null, })); await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); } @@ -8401,7 +8337,6 @@ export class OrganizationController extends Controller { if (nextHolderIds.length > 0) { // FIX: Fetch positions first before updating (to avoid race condition) const posMastersToUpdate = await queryRunner.manager.find(PosMaster, { - relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], where: { orgRevisionId: currentRevisionId, current_holderId: In(nextHolderIds), @@ -8414,34 +8349,7 @@ export class OrganizationController extends Controller { .map((pos) => ({ posMasterDnaId: pos.ancestorDNA, profileId: null, - pm: { - prefix: null, - firstName: null, - lastName: null, - position: null, - posType: null, - posLevel: null, - posExecutive: null, - profileId: null, - shortName: pos - ? [ - pos.orgChild4?.orgChild4ShortName, - pos.orgChild3?.orgChild3ShortName, - pos.orgChild2?.orgChild2ShortName, - pos.orgChild1?.orgChild1ShortName, - pos.orgRoot?.orgRootShortName, - ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? - null - : null, - posMasterNoPrefix: pos.posMasterNoPrefix ?? null, - posMasterNo: pos.posMasterNo != null ? String(pos.posMasterNo) : null, - posMasterNoSuffix: pos.posMasterNoSuffix ?? null, - rootDnaId: pos?.orgRoot?.ancestorDNA ?? null, - child1DnaId: pos?.orgChild1?.ancestorDNA ?? null, - child2DnaId: pos?.orgChild2?.ancestorDNA ?? null, - child3DnaId: pos?.orgChild3?.ancestorDNA ?? null, - child4DnaId: pos?.orgChild4?.ancestorDNA ?? null, - } as SavePosMasterHistory, + pm: null, })); await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps); @@ -8458,7 +8366,6 @@ export class OrganizationController extends Controller { // 2.2 Fetch current positions for comparison const posMasterCurrent = await this.posMasterRepository.find({ - relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], where: [ { orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) }, { orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) }, @@ -8488,49 +8395,19 @@ export class OrganizationController extends Controller { const deleteHistoryOps = toDelete.map((pos) => ({ posMasterDnaId: pos.ancestorDNA, profileId: null, - pm: { - prefix: null, - firstName: null, - lastName: null, - position: null, - posType: null, - posLevel: null, - posExecutive: null, - profileId: null, - shortName: pos - ? [ - pos.orgChild4?.orgChild4ShortName, - pos.orgChild3?.orgChild3ShortName, - pos.orgChild2?.orgChild2ShortName, - pos.orgChild1?.orgChild1ShortName, - pos.orgRoot?.orgRootShortName, - ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? - null - : null, - posMasterNoPrefix: pos.posMasterNoPrefix ?? null, - posMasterNo: pos.posMasterNo != null ? String(pos.posMasterNo) : null, - posMasterNoSuffix: pos.posMasterNoSuffix ?? null, - rootDnaId: pos?.orgRoot?.ancestorDNA ?? null, - child1DnaId: pos?.orgChild1?.ancestorDNA ?? null, - child2DnaId: pos?.orgChild2?.ancestorDNA ?? null, - child3DnaId: pos?.orgChild3?.ancestorDNA ?? null, - child4DnaId: pos?.orgChild4?.ancestorDNA ?? null, - } as SavePosMasterHistory, + pm: null, })); await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps); } // 2.4 Process draft positions (UPDATE or INSERT) - const toUpdate: Partial[] = []; + const toUpdate: PosMaster[] = []; const toInsert: any[] = []; // Track draft PosMaster ID to current PosMaster ID mapping for position sync // Type: Map const posMasterMapping: Map = new Map(); - // Collect positions where next_holderId is null for batch history saving - const nullHolderDraftPosIds: string[] = []; - for (const draftPos of posMasterDraft) { const current = currentByDNA.get(draftPos.ancestorDNA); @@ -8543,9 +8420,7 @@ export class OrganizationController extends Controller { if (current) { // UPDATE existing position - toUpdate.push({ - id: current.id, - ancestorDNA: current.ancestorDNA, + Object.assign(current, { createdAt: draftPos.createdAt, createdUserId: draftPos.createdUserId, createdFullName: draftPos.createdFullName, @@ -8556,14 +8431,12 @@ export class OrganizationController extends Controller { posMasterNoSuffix: draftPos.posMasterNoSuffix, posMasterNo: draftPos.posMasterNo, posMasterOrder: draftPos.posMasterOrder, - orgRevisionId: currentRevisionId, orgRootId, orgChild1Id, orgChild2Id, orgChild3Id, orgChild4Id, current_holderId: draftPos.next_holderId, - next_holderId: draftPos.next_holderId, isSit: draftPos.isSit, reason: draftPos.reason, isDirector: draftPos.isDirector, @@ -8573,9 +8446,10 @@ export class OrganizationController extends Controller { isCondition: draftPos.isCondition, conditionReason: draftPos.conditionReason, }); + toUpdate.push(current); if (draftPos.next_holderId === null) { - nullHolderDraftPosIds.push(draftPos.id); + await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null); } // Track mapping for position sync @@ -8601,7 +8475,7 @@ export class OrganizationController extends Controller { // Batch save updates and inserts if (toUpdate.length > 0) { - await queryRunner.manager.save(PosMaster, toUpdate); + await queryRunner.manager.save(toUpdate); } if (toInsert.length > 0) { const saved = await queryRunner.manager.save(toInsert); @@ -8618,62 +8492,6 @@ export class OrganizationController extends Controller { } } - // 2.4.1 Save PosMasterHistory for positions where next_holderId was cleared (null) - // These need org relations to populate shortName, rootDnaId, child*DnaId fields - if (nullHolderDraftPosIds.length > 0) { - const nullHolderCurrentPosIds = nullHolderDraftPosIds - .map((draftPosId) => posMasterMapping.get(draftPosId)?.[0] ?? null) - .filter((currentPosId): currentPosId is string => currentPosId !== null); - - const nullHolderPosMasters = await queryRunner.manager.find(PosMaster, { - where: { id: In(nullHolderCurrentPosIds) }, - relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], - }); - const nullHolderMap = new Map(nullHolderPosMasters.map((pm) => [pm.id, pm as any])); - - const nullHolderHistoryOps = posMasterDraft - .filter((d) => nullHolderDraftPosIds.includes(d.id)) - .map((draftPos) => { - const currentPosId = posMasterMapping.get(draftPos.id)?.[0] ?? null; - const pmWithRelations = currentPosId ? nullHolderMap.get(currentPosId) : null; - return { - posMasterDnaId: draftPos.ancestorDNA, - profileId: null as string | null, - pm: { - prefix: null, - firstName: null, - lastName: null, - position: null, - posType: null, - posLevel: null, - posExecutive: null, - profileId: null, - shortName: pmWithRelations - ? [ - pmWithRelations.orgChild4?.orgChild4ShortName, - pmWithRelations.orgChild3?.orgChild3ShortName, - pmWithRelations.orgChild2?.orgChild2ShortName, - pmWithRelations.orgChild1?.orgChild1ShortName, - pmWithRelations.orgRoot?.orgRootShortName, - ].find( - (s: string | undefined) => typeof s === "string" && s.trim().length > 0, - ) ?? null - : null, - posMasterNoPrefix: draftPos.posMasterNoPrefix ?? null, - posMasterNo: draftPos.posMasterNo != null ? String(draftPos.posMasterNo) : null, - posMasterNoSuffix: draftPos.posMasterNoSuffix ?? null, - rootDnaId: pmWithRelations?.orgRoot?.ancestorDNA ?? null, - child1DnaId: pmWithRelations?.orgChild1?.ancestorDNA ?? null, - child2DnaId: pmWithRelations?.orgChild2?.ancestorDNA ?? null, - child3DnaId: pmWithRelations?.orgChild3?.ancestorDNA ?? null, - child4DnaId: pmWithRelations?.orgChild4?.ancestorDNA ?? null, - } as SavePosMasterHistory, - }; - }); - - await BatchSavePosMasterHistoryOfficer(queryRunner, nullHolderHistoryOps); - } - // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) const positionSyncStats = await this.syncAllPositionsBatch( queryRunner, @@ -8722,99 +8540,6 @@ export class OrganizationController extends Controller { return mapping.byDraftId.get(draftId) ?? null; } - private resolveRequiredOrgId( - draftId: string | null | undefined, - mapping: OrgIdMapping, - fieldName: string, - entityName: string, - entityDna: string, - rootDnaId: string, - ): string | null { - if (!draftId) return null; - - const mappedId = mapping.byDraftId.get(draftId) ?? null; - if (mappedId) return mappedId; - - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - `ไม่สามารถ map ${fieldName} ของ ${entityName} (${entityDna}) ใน rootDnaId ${rootDnaId} ได้`, - ); - } - - private getMappedParentIds( - entityClass: any, - draftNode: any, - parentMappings: AllOrgMappings | undefined, - rootDnaId: string, - ): Partial<{ - orgRootId: string | null; - orgChild1Id: string | null; - orgChild2Id: string | null; - orgChild3Id: string | null; - }> { - if (entityClass === OrgRoot) { - return {}; - } - - if (!parentMappings) { - throw new HttpError( - HttpStatusCode.INTERNAL_SERVER_ERROR, - `ไม่พบข้อมูล mapping ของโครงสร้างสำหรับ rootDnaId ${rootDnaId}`, - ); - } - - const mappedParentIds: Partial<{ - orgRootId: string | null; - orgChild1Id: string | null; - orgChild2Id: string | null; - orgChild3Id: string | null; - }> = {}; - - mappedParentIds.orgRootId = this.resolveRequiredOrgId( - draftNode.orgRootId, - parentMappings.orgRoot, - "orgRootId", - entityClass.name, - draftNode.ancestorDNA, - rootDnaId, - ); - - if (entityClass === OrgChild2 || entityClass === OrgChild3 || entityClass === OrgChild4) { - mappedParentIds.orgChild1Id = this.resolveRequiredOrgId( - draftNode.orgChild1Id, - parentMappings.orgChild1, - "orgChild1Id", - entityClass.name, - draftNode.ancestorDNA, - rootDnaId, - ); - } - - if (entityClass === OrgChild3 || entityClass === OrgChild4) { - mappedParentIds.orgChild2Id = this.resolveRequiredOrgId( - draftNode.orgChild2Id, - parentMappings.orgChild2, - "orgChild2Id", - entityClass.name, - draftNode.ancestorDNA, - rootDnaId, - ); - } - - if (entityClass === OrgChild4) { - mappedParentIds.orgChild3Id = this.resolveRequiredOrgId( - draftNode.orgChild3Id, - parentMappings.orgChild3, - "orgChild3Id", - entityClass.name, - draftNode.ancestorDNA, - rootDnaId, - ); - } - - return mappedParentIds; - } - /** * Helper function: Cascade delete positions before deleting org node */ @@ -8853,7 +8578,6 @@ export class OrganizationController extends Controller { repository: any, draftRevisionId: string, currentRevisionId: string, - rootDnaId: string, parentMappings?: AllOrgMappings, draftOrgRootId?: string, currentOrgRootId?: string, @@ -8926,9 +8650,53 @@ export class OrganizationController extends Controller { ...draft, id: current.id, orgRevisionId: currentRevisionId, - ...this.getMappedParentIds(entityClass, draft, parentMappings, rootDnaId), }; + // Map parent IDs based on entity level + if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) { + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } else if (entityClass === OrgChild2) { + if (draft.orgRootId && parentMappings) { + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } + if (draft.orgChild1Id && parentMappings) { + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + } + } else if (entityClass === OrgChild3) { + if (draft.orgRootId && parentMappings) { + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } + if (draft.orgChild1Id && parentMappings) { + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + } + if (draft.orgChild2Id && parentMappings) { + updateData.orgChild2Id = + parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + } + } else if (entityClass === OrgChild4) { + if (draft.orgRootId && parentMappings) { + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + } + if (draft.orgChild1Id && parentMappings) { + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + } + if (draft.orgChild2Id && parentMappings) { + updateData.orgChild2Id = + parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + } + if (draft.orgChild3Id && parentMappings) { + updateData.orgChild3Id = + parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id) ?? draft.orgChild3Id; + } + } + await queryRunner.manager.update(entityClass, current.id, updateData); mapping.byAncestorDNA.set(draft.ancestorDNA, current.id); @@ -8942,9 +8710,77 @@ export class OrganizationController extends Controller { ...draft, id: undefined, orgRevisionId: currentRevisionId, - ...this.getMappedParentIds(entityClass, draft, parentMappings, rootDnaId), }); + // Map parent IDs based on entity level + if (entityClass === OrgChild1 && draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + } else if (entityClass === OrgChild2) { + if (draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + } + if (draft.orgChild1Id && parentMappings) { + const mappedChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + if (mappedChild1Id) { + newNode.orgChild1Id = mappedChild1Id; + } + } + } else if (entityClass === OrgChild3) { + if (draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + } + if (draft.orgChild1Id && parentMappings) { + const mappedChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + if (mappedChild1Id) { + newNode.orgChild1Id = mappedChild1Id; + } + } + if (draft.orgChild2Id && parentMappings) { + const mappedChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + if (mappedChild2Id) { + newNode.orgChild2Id = mappedChild2Id; + } + } + } else if (entityClass === OrgChild4) { + if (draft.orgRootId) { + if (draft.orgRootId === draftOrgRootId) { + newNode.orgRootId = currentOrgRootId; + } else if (parentMappings) { + newNode.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId); + } + } + if (draft.orgChild1Id && parentMappings) { + const mappedChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id); + if (mappedChild1Id) { + newNode.orgChild1Id = mappedChild1Id; + } + } + if (draft.orgChild2Id && parentMappings) { + const mappedChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id); + if (mappedChild2Id) { + newNode.orgChild2Id = mappedChild2Id; + } + } + if (draft.orgChild3Id && parentMappings) { + const mappedChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id); + if (mappedChild3Id) { + newNode.orgChild3Id = mappedChild3Id; + } + } + } + const saved = await queryRunner.manager.save(newNode); mapping.byAncestorDNA.set(draft.ancestorDNA, saved.id); @@ -9000,7 +8836,6 @@ export class OrganizationController extends Controller { where: { posMasterId: In(currentPosMasterIds), }, - relations: ["posType", "posLevel", "posExecutive"], }), ]); @@ -9028,12 +8863,6 @@ export class OrganizationController extends Controller { const allToInsert: Array = []; const profileUpdates: Map = new Map(); - // Collect position and posMaster data for delete history tracking - const deleteHistoryData: Array<{ - position: any; - posMaster: PosMaster; - }> = []; - // Create a map for quick lookup of draft PosMasters with relations const draftPosMasterMap = new Map(draftPosMasters.map((pm: PosMaster) => [pm.id, pm])); @@ -9053,11 +8882,6 @@ export class OrganizationController extends Controller { if (draftPositions.length === 0) { allToDelete.push(...currentPositions.map((p: any) => p.id)); allToDeleteHistory.push(...currentPositions.map((p: any) => p.ancestorDNA)); - // Collect data for history tracking - const pm = draftPosMasterMap.get(draftPosMasterId) as PosMaster; - for (const pos of currentPositions) { - deleteHistoryData.push({ position: pos, posMaster: pm }); - } continue; } @@ -9066,13 +8890,10 @@ export class OrganizationController extends Controller { const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); // Mark for deletion: current positions not in draft (by orderNo) - const pm = draftPosMasterMap.get(draftPosMasterId) as PosMaster; for (const currentPos of currentPositions) { if (!draftOrderNos.has(currentPos.orderNo)) { allToDelete.push(currentPos.id); allToDeleteHistory.push(currentPos.ancestorDNA); - // Collect data for history tracking - deleteHistoryData.push({ position: currentPos, posMaster: pm }); } } @@ -9112,27 +8933,13 @@ export class OrganizationController extends Controller { const draftPosMaster = draftPosMasterMap.get(draftPosMasterId) as any; // Collect profile update for the selected position - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - if (nextHolderId != null && draftPos.positionIsSelected) { - const _null: any = null; - profileUpdates.set(nextHolderId, { - posMasterNo: draftPosMaster - ? getPosMasterNo(draftPosMaster as PosMaster) ?? _null - : _null, - org: draftPosMaster ? getOrgFullName(draftPosMaster as PosMaster) ?? _null : _null, - }); - } // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (nextHolderId != null && draftPos.positionIsSelected && !draftPosMaster?.isSit) { - const existing = profileUpdates.get(nextHolderId) || {}; - existing.position = draftPos.positionName; - existing.posTypeId = draftPos.posTypeId; - existing.posLevelId = draftPos.posLevelId; - existing.positionField = draftPos.positionField ?? null; - existing.posExecutive = (draftPos as any).posExecutive?.posExecutiveName ?? null; - existing.positionArea = draftPos.positionArea ?? null; - existing.positionExecutiveField = draftPos.positionExecutiveField ?? null; - profileUpdates.set(nextHolderId, existing); + profileUpdates.set(nextHolderId, { + position: draftPos.positionName, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + }); if (draftPosMaster && draftPosMaster.ancestorDNA) { // Find the selected position from draft positions const selectedPos = @@ -9180,36 +8987,10 @@ export class OrganizationController extends Controller { // Bulk DELETE if (allToDelete.length > 0) { await queryRunner.manager.delete(Position, allToDelete); - const deleteOps = deleteHistoryData.map(({ position, posMaster }) => ({ - posMasterDnaId: position.ancestorDNA, + const deleteOps = allToDeleteHistory.map((ancestorDNA) => ({ + posMasterDnaId: ancestorDNA, profileId: null, - pm: { - prefix: null, - firstName: null, - lastName: null, - position: null, - posType: null, - posLevel: null, - posExecutive: null, - profileId: null, - rootDnaId: posMaster?.orgRoot?.ancestorDNA ?? null, - child1DnaId: posMaster?.orgChild1?.ancestorDNA ?? null, - child2DnaId: posMaster?.orgChild2?.ancestorDNA ?? null, - child3DnaId: posMaster?.orgChild3?.ancestorDNA ?? null, - child4DnaId: posMaster?.orgChild4?.ancestorDNA ?? null, - shortName: posMaster - ? [ - posMaster.orgChild4?.orgChild4ShortName, - posMaster.orgChild3?.orgChild3ShortName, - posMaster.orgChild2?.orgChild2ShortName, - posMaster.orgChild1?.orgChild1ShortName, - posMaster.orgRoot?.orgRootShortName, - ].find((s) => typeof s === "string" && s.trim().length > 0) ?? null - : null, - posMasterNoPrefix: posMaster?.posMasterNoPrefix ?? null, - posMasterNo: posMaster?.posMasterNo != null ? String(posMaster.posMasterNo) : null, - posMasterNoSuffix: posMaster?.posMasterNoSuffix ?? null, - }, + pm: null, })); await BatchSavePosMasterHistoryOfficer(queryRunner, deleteOps); deletedCount = allToDelete.length; diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index fce9bb98..4277a917 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -26,7 +26,6 @@ import { OrgRoot } from "../entities/OrgRoot"; import { Position } from "../entities/Position"; import { PosMaster } from "../entities/PosMaster"; import { PosMasterHistory } from "../entities/PosMasterHistory"; -import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; import { Profile } from "../entities/Profile"; import { ProfileEducation } from "../entities/ProfileEducation"; import { ProfileEmployee } from "../entities/ProfileEmployee"; @@ -58,7 +57,6 @@ export class OrganizationDotnetController extends Controller { private positionRepository = AppDataSource.getRepository(Position); private posMasterRepository = AppDataSource.getRepository(PosMaster); private posMasterHistoryRepository = AppDataSource.getRepository(PosMasterHistory); - private posMasterEmployeeHistoryRepository = AppDataSource.getRepository(PosMasterEmployeeHistory); private empPosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); private insigniaRepo = AppDataSource.getRepository(ProfileInsignia); private employeePosDictRepository = AppDataSource.getRepository(EmployeePosDict); @@ -2352,131 +2350,6 @@ export class OrganizationDotnetController extends Controller { return new HttpSuccess(mapProfile); } - /** - * API Get Profile For Process Check In - * @summary API Get Profile For Process Check In - * @param {string} keycloakId keycloakId profile - */ - @Get("check-keycloak/{keycloakId}") - async GetProfileForProcessCheckInAsync(@Path() keycloakId: string) { - try { - /* ========================= - * 1. Load profile (Officer) - * ========================= */ - const profile = await this.profileRepo.findOne({ - where: { keycloak: keycloakId }, - relations: { - current_holders: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }, - }); - - // Employee - if (!profile) { - const empProfile = await this.profileEmpRepo.findOne({ - where: { keycloak: keycloakId }, - relations: { - current_holders: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }, - }); - if (!empProfile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - const currentHolder = empProfile.current_holders?.find( - (x) => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, - ); - - const mapProfile = { - profileType: "EMPLOYEE", - id: empProfile.id, - keycloak: empProfile.keycloak, - prefix: empProfile.prefix, - firstName: empProfile.firstName, - lastName: empProfile.lastName, - citizenId: empProfile.citizenId, - gender: empProfile.gender, - - root: currentHolder?.orgRoot?.orgRootName ?? null, - rootId: currentHolder?.orgRootId ?? null, - rootDnaId: currentHolder?.orgRoot?.ancestorDNA ?? null, - child1: currentHolder?.orgChild1?.orgChild1Name ?? null, - child1Id: currentHolder?.orgChild1Id ?? null, - child1DnaId: currentHolder?.orgChild1?.ancestorDNA ?? null, - child2: currentHolder?.orgChild2?.orgChild2Name ?? null, - child2Id: currentHolder?.orgChild2Id ?? null, - child2DnaId: currentHolder?.orgChild2?.ancestorDNA ?? null, - child3: currentHolder?.orgChild3?.orgChild3Name ?? null, - child3Id: currentHolder?.orgChild3Id ?? null, - child3DnaId: currentHolder?.orgChild3?.ancestorDNA ?? null, - child4: currentHolder?.orgChild4?.orgChild4Name ?? null, - child4Id: currentHolder?.orgChild4Id ?? null, - child4DnaId: currentHolder?.orgChild4?.ancestorDNA ?? null, - }; - - return new HttpSuccess(mapProfile); - } - - /* ========================================= - * 2. current holder (Officer) - * ========================================= */ - const currentHolder = profile.current_holders?.find( - (x) => - x.orgRevision?.orgRevisionIsDraft === false && x.orgRevision?.orgRevisionIsCurrent === true, - ); - - /* ========================================= - * 3. map response - * ========================================= */ - const mapProfile = { - profileType: "OFFICER", - id: profile.id, - keycloak: profile.keycloak, - prefix: profile.prefix, - firstName: profile.firstName, - lastName: profile.lastName, - citizenId: profile.citizenId, - gender: profile.gender, - - root: currentHolder?.orgRoot?.orgRootName ?? null, - rootId: currentHolder?.orgRootId ?? null, - rootDnaId: currentHolder?.orgRoot?.ancestorDNA ?? null, - child1: currentHolder?.orgChild1?.orgChild1Name ?? null, - child1Id: currentHolder?.orgChild1Id ?? null, - child1DnaId: currentHolder?.orgChild1?.ancestorDNA ?? null, - child2: currentHolder?.orgChild2?.orgChild2Name ?? null, - child2Id: currentHolder?.orgChild2Id ?? null, - child2DnaId: currentHolder?.orgChild2?.ancestorDNA ?? null, - child3: currentHolder?.orgChild3?.orgChild3Name ?? null, - child3Id: currentHolder?.orgChild3Id ?? null, - child3DnaId: currentHolder?.orgChild3?.ancestorDNA ?? null, - child4: currentHolder?.orgChild4?.orgChild4Name ?? null, - child4Id: currentHolder?.orgChild4Id ?? null, - child4DnaId: currentHolder?.orgChild4?.ancestorDNA ?? null, - }; - - return new HttpSuccess(mapProfile); - } catch (error: any) { - // Log เฉพาะ unexpected errors (ไม่ใช่ HttpError) - if (!(error instanceof HttpError)) { - console.error(`[check-keycloak] Unexpected error: keycloakId=${keycloakId}`, error); - } - throw error; - } - } - /** * API Get Profile For Logs * @@ -6924,273 +6797,229 @@ export class OrganizationDotnetController extends Controller { return new HttpSuccess(profile_); } - /** - * รายชื่อลูกจ้างประจำ ตามสิทธิ์ admin - * @summary รายชื่อลูกจ้างประจำ ตามสิทธิ์ admin - */ - @Post("employee-by-admin-rolev2") - async GetEmployeesByAdminRoleV2( - @Request() req: RequestWithUser, - @Body() - body: { - node: number; - nodeId: string; - role: string; - isRetirement?: boolean; - reqNode?: number; - reqNodeId?: string; - date: Date; - }, - ) { - let typeCondition: any = {}; - if (body.role === "CHILD" || body.role === "BROTHER") { - if (body.role === "CHILD") { - switch (body.node) { - case 0: - typeCondition = { - rootDnaId: body.nodeId, - }; - break; - case 1: - typeCondition = { - child1DnaId: body.nodeId, - }; - break; - case 2: - typeCondition = { - child2DnaId: body.nodeId, - }; - break; - case 3: - typeCondition = { - child3DnaId: body.nodeId, - }; - break; - case 4: - typeCondition = { - child4DnaId: body.nodeId, - }; - break; - default: - typeCondition = {}; - break; - } - } else if (body.role === "BROTHER") { - switch (body.node) { - case 0: - typeCondition = { - rootDnaId: body.nodeId, - }; - break; - case 1: - typeCondition = { - rootDnaId: body.nodeId, - }; - break; - case 2: - typeCondition = { - child1DnaId: body.nodeId, - }; - break; - case 3: - typeCondition = { - child2DnaId: body.nodeId, - }; - break; - case 4: - typeCondition = { - child3DnaId: body.nodeId, - }; - break; - default: - typeCondition = {}; - break; - } - } - } else if (body.role === "OWNER" || body.role === "ROOT" || body.role === "PARENT") { - 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; - } - } else if (body.role === "NORMAL") { - switch (body.node) { - case 0: - typeCondition = { - rootDnaId: body.nodeId, - child1DnaId: IsNull(), - }; - break; - case 1: - typeCondition = { - child1DnaId: body.nodeId, - child2DnaId: IsNull(), - }; - break; - case 2: - typeCondition = { - child2DnaId: body.nodeId, - child3DnaId: IsNull(), - }; - break; - case 3: - typeCondition = { - child3DnaId: body.nodeId, - child4DnaId: IsNull(), - }; - break; - case 4: - typeCondition = { - child4DnaId: body.nodeId, - }; - break; - default: - typeCondition = {}; - break; - } - } - // set เวลาเป็น 23:59:59 ของวันนั้น - const date = body.date ? new Date(body.date.toISOString().slice(0, 10)) : new Date(); - date.setHours(23, 59, 59, 999); + // /** + // * รายชื่อขรก. ตามสิทธิ์ admin + // * + // * @summary รายชื่อขรก. ตามสิทธิ์ admin + // * + // */ + // @Post("employee-by-admin-rolev2") + // async GetEmployeesByAdminRoleV2( + // @Request() req: RequestWithUser, + // @Body() + // body: { + // node: number; + // nodeId: string; + // role: string; + // isRetirement?: boolean; + // reqNode?: number; + // reqNodeId?: string; + // date?: Date; + // }, + // ) { + // let typeCondition: any = {}; + // if (body.role === "CHILD" || body.role === "PARENT" || body.role === "BROTHER") { + // if (body.role === "CHILD") { + // switch (body.node) { + // case 0: + // typeCondition = { + // rootDnaId: body.nodeId, + // }; + // break; + // case 1: + // typeCondition = { + // child1DnaId: body.nodeId, + // }; + // break; + // case 2: + // typeCondition = { + // child2DnaId: body.nodeId, + // }; + // break; + // case 3: + // typeCondition = { + // child3DnaId: body.nodeId, + // }; + // break; + // case 4: + // typeCondition = { + // child4DnaId: body.nodeId, + // }; + // break; + // default: + // typeCondition = {}; + // break; + // } + // } else if (body.role === "BROTHER") { + // switch (body.node) { + // case 0: + // typeCondition = { + // rootDnaId: body.nodeId, + // }; + // break; + // case 1: + // typeCondition = { + // rootDnaId: body.nodeId, + // }; + // break; + // case 2: + // typeCondition = { + // child1DnaId: body.nodeId, + // }; + // break; + // case 3: + // typeCondition = { + // child2DnaId: body.nodeId, + // }; + // break; + // case 4: + // typeCondition = { + // child3DnaId: body.nodeId, + // }; + // break; + // default: + // typeCondition = {}; + // break; + // } + // } else if (body.role === "PARENT") { + // typeCondition = { + // rootDnaId: body.nodeId, + // child1DnaId: Not(IsNull()), + // }; + // } + // } else if (body.role === "OWNER" || body.role === "ROOT") { + // 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; + // } + // } else if (body.role === "NORMAL") { + // switch (body.node) { + // case 0: + // typeCondition = { + // rootDnaId: body.nodeId, + // child1DnaId: IsNull(), + // }; + // break; + // case 1: + // typeCondition = { + // child1DnaId: body.nodeId, + // child2DnaId: IsNull(), + // }; + // break; + // case 2: + // typeCondition = { + // child2DnaId: body.nodeId, + // child3DnaId: IsNull(), + // }; + // break; + // case 3: + // typeCondition = { + // child3DnaId: body.nodeId, + // child4DnaId: IsNull(), + // }; + // break; + // case 4: + // typeCondition = { + // child4DnaId: body.nodeId, + // }; + // break; + // default: + // typeCondition = {}; + // break; + // } + // } + // const date = body.date ? new Date(body.date) : new Date(); + // // set เวลาเป็น 23:59:59 ของวันนั้น + // date.setHours(23, 59, 59, 999); - let posEmpHis = await this.posMasterEmployeeHistoryRepository.find({ - where: { - ...typeCondition, - createdAt: LessThanOrEqual(date), - }, - select: [ - "profileEmployeeId", - "prefix", - "firstName", - "lastName", - "shortName", - "posMasterNo", - "position", - "posType", - "posLevel", - "ancestorDNA", - "rootDnaId", - "child1DnaId", - "child2DnaId", - "child3DnaId", - "child4DnaId", - "createdAt", - ], - order: { - firstName: "ASC", - lastName: "ASC", - createdAt: "DESC", - }, - }); + // let profile = await this.posMasterEmployeeHistoryRepository.find({ + // where: { + // ...typeCondition, + // createdAt: LessThanOrEqual(date), + // // firstName: Not("") && Not(IsNull()), + // // lastName: Not("") && Not(IsNull()), + // }, + // order: { + // firstName: "ASC", + // lastName: "ASC", + // createdAt: "DESC", // ให้ createdAt ล่าสุดอยู่ข้างบน + // }, + // }); - // group1: group by ancestorDNA แล้วเลือก create_at ล่าสุด - const grouped1 = new Map(); - for (const item of posEmpHis) { - const key = `${item.ancestorDNA}`; - if (!grouped1.has(key)) { - grouped1.set(key, item); - } else { - // ถ้าเจอซ้ำ ให้เลือก createdAt ล่าสุด - 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(); - for (const item of Array.from(grouped1.values())) { - const key = `${item.shortName}-${item.posMasterNo}`; - if (!grouped2.has(key)) { - grouped2.set(key, item); - } else { - // ถ้าเจอซ้ำ ให้เลือก createdAt ล่าสุด - 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(); - for (const item of Array.from(grouped2.values())) { - const key = `${item.firstName}-${item.lastName}`; - if (!grouped3.has(key)) { - grouped3.set(key, item); - } else { - // ถ้าเจอซ้ำ ให้เลือก createdAt ล่าสุด - const exist = grouped3.get(key); - if (exist && item.createdAt > exist.createdAt) { - grouped3.set(key, item); - } - } - } + // // group by ancestorDNA แล้วเลือก create_at ล่าสุด + // const grouped = new Map(); + // for (const item of profile) { + // const key = `${item.shortName}-${item.posMasterNo}`; + // if (!grouped.has(key)) { + // grouped.set(key, item); + // } else { + // // ถ้าเจอซ้ำ ให้เลือก createdAt ล่าสุด + // const exist = grouped.get(key); + // if (exist && item.createdAt > exist.createdAt) { + // grouped.set(key, item); + // } + // } + // } - const profileEmployeeIds = Array.from(grouped3.values()) - .filter((x) => x.profileEmployeeId != null) - .map((x) => x.profileEmployeeId); + // const profile_ = await Promise.all( + // Array.from(grouped.values()) + // .filter((x) => x.profileId != null) + // .map(async (item: PosMasterEmployeeHistory) => { + // let profile = await this.profileRepo.findOne({ + // where: { id: item.profileId }, + // }); - const profileEmployees = await this.profileEmpRepo.find({ - where: { id: In(profileEmployeeIds) }, - select: ["id", "citizenId", "dateStart", "dateAppoint", "keycloak"], - }); + // 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, + // position: item.position, + // positionLevel: item.posLevel, + // positionType: item.posType, + // // oc: Oc, + // orgRootId: item.rootDnaId, + // orgChild1Id: item.child1DnaId, + // orgChild2Id: item.child2DnaId, + // orgChild3Id: item.child3DnaId, + // orgChild4Id: item.child4DnaId, + // }; + // }), + // ); - const profileEmployeeMap = new Map(profileEmployees.map((p) => [p.id, p])); - - const profile_ = Array.from(grouped3.values()) - .filter((x) => x.profileEmployeeId != null) - .map((item: PosMasterEmployeeHistory) => { - const profileEmp = profileEmployeeMap.get(item.profileEmployeeId); - return { - id: item.profileEmployeeId, - prefix: item.prefix, - firstName: item.firstName, - lastName: item.lastName, - citizenId: profileEmp?.citizenId ?? null, - dateStart: profileEmp?.dateStart ?? null, - dateAppoint: profileEmp?.dateAppoint ?? null, - keycloak: profileEmp?.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 })), - ); - } + // return new HttpSuccess(profile_); + // } /** * 4. API Update รอบการลงเวลา ในตาราง profile @@ -8652,7 +8481,6 @@ export class OrganizationDotnetController extends Controller { break; } } else if (body.role === "BROTHER") { - // nodeId ที่รับมาเป็น DNA ของระดับพ่อแม่ (สูงกว่า 1 ระดับ) จึงต้อง query ด้วย field ของระดับพ่อแม่ switch (body.node) { case 0: typeCondition = { @@ -8765,29 +8593,13 @@ export class OrganizationDotnetController extends Controller { where: { ...typeCondition, createdAt: LessThanOrEqual(date), + // firstName: Not("") && Not(IsNull()), + // lastName: Not("") && Not(IsNull()), }, - select: [ - "profileId", - "prefix", - "firstName", - "lastName", - "shortName", - "posMasterNo", - "position", - "posType", - "posLevel", - "ancestorDNA", - "rootDnaId", - "child1DnaId", - "child2DnaId", - "child3DnaId", - "child4DnaId", - "createdAt", - ], order: { firstName: "ASC", lastName: "ASC", - createdAt: "DESC", + createdAt: "DESC", // ให้ createdAt ล่าสุดอยู่ข้างบน }, }); @@ -8834,41 +8646,36 @@ export class OrganizationDotnetController extends Controller { } } - const profileIds = Array.from(grouped3.values()) - .filter((x) => x.profileId != null) - .map((x) => x.profileId); + 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 }, + }); - const profiles = await this.profileRepo.find({ - where: { id: In(profileIds) }, - select: ["id", "citizenId", "dateStart", "dateAppoint", "keycloak"], - }); - - const profileMap = new Map(profiles.map((p) => [p.id, p])); - - const profile_ = Array.from(grouped3.values()) - .filter((x) => x.profileId != null) - .map((item: PosMasterHistory) => { - const profile = profileMap.get(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 { + 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, + // oc: Oc, + 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 })), @@ -8891,16 +8698,7 @@ export class OrganizationDotnetController extends Controller { ) { const profile = await this.profileRepo.findOne({ where: { id: requestBody.profileId }, - relations: { - current_holders: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true - } - } + relations: ["current_holders", "current_holders.orgRevision"], }); if (!profile) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); @@ -8935,21 +8733,10 @@ export class OrganizationDotnetController extends Controller { "orgChild2.ancestorDNA AS child2DnaId", "orgChild3.ancestorDNA AS child3DnaId", "orgChild4.ancestorDNA AS child4DnaId", - "authRoleAttr.attrPrivilege AS attrPrivilege", ]) .distinct(true) // ต้องมี posMasterAssign .innerJoin("posMasterAssign", "assign", "assign.posMasterId = pm.id") - // INNER JOIN เพื่อเอาเฉพาะที่มี attrPrivilege - .innerJoin("pm.authRole", "authRole") - .innerJoin( - "authRole.authRoles", "authRoleAttr", - "authRoleAttr.authSysId = :authSysId AND authRoleAttr.attrIsList = :attrIsList", - { - attrIsList: true, - authSysId: assign.id - } - ) // join เพื่อเอา ancestorDNA .leftJoin("pm.orgRoot", "orgRoot") .leftJoin("pm.orgChild1", "orgChild1") @@ -8971,123 +8758,6 @@ export class OrganizationDotnetController extends Controller { }) .getRawMany(); - // ──────────────────────────────────────────────────────── - // กรองตามสิทธิ์ (NORMAL, CHILD, BROTHER) - // ROOT และ PARENT ให้ผ่านทุกคน เพราะ filter orgRootId อยู่แล้ว - // ──────────────────────────────────────────────────────── - - // 1. หา User Node - const userNode = currentHolder.orgChild4Id ? 4 - : currentHolder.orgChild3Id ? 3 - : currentHolder.orgChild2Id ? 2 - : currentHolder.orgChild1Id ? 1 - : 0; - - // 2. หา User DNA แต่ละระดับ - const userDna = { - root: currentHolder.orgRoot?.ancestorDNA ?? null, - child1: currentHolder.orgChild1?.ancestorDNA ?? null, - child2: currentHolder.orgChild2?.ancestorDNA ?? null, - child3: currentHolder.orgChild3?.ancestorDNA ?? null, - child4: currentHolder.orgChild4?.ancestorDNA ?? null, - }; - - // 3. กรอง posMasters ตามสิทธิ์ - const filteredPosMasters = posMasters.filter((staff) => { - const privilege = staff.attrPrivilege; - - // ROOT และ PARENT: ให้ผ่านทุกคน เพราะ filter orgRootId อยู่แล้ว - if (privilege === "ROOT" || privilege === "PARENT" || privilege === "OWNER") { - return true; - } - - // หา Staff Node - const staffNode = staff.orgChild4Id ? 4 - : staff.orgChild3Id ? 3 - : staff.orgChild2Id ? 2 - : staff.orgChild1Id ? 1 - : 0; - - // หา Staff DNA - const staffDna = { - root: staff.rootDnaId, - child1: staff.child1DnaId, - child2: staff.child2DnaId, - child3: staff.child3DnaId, - child4: staff.child4DnaId, - }; - - // NORMAL: Node เท่ากัน + DNA เหมือนกันทุกตัว - if (privilege === "NORMAL") { - return ( - staffNode === userNode && - staffDna.root === userDna.root && - (staffNode < 1 || staffDna.child1 === userDna.child1) && - (staffNode < 2 || staffDna.child2 === userDna.child2) && - (staffNode < 3 || staffDna.child3 === userDna.child3) && - (staffNode < 4 || staffDna.child4 === userDna.child4) - ); - } - - // CHILD: Staff เห็น User ที่อยู่ในกิ่งลูก - if (privilege === "CHILD") { - // Staff ต้องอยู่บนกว่าหรือเท่ากับ User - if (staffNode > userNode) return false; - - switch (staffNode) { - case 0: - if (staffDna.root !== userDna.root) return false; - break; - case 1: - if (staffDna.root !== userDna.root) return false; - if (staffDna.child1 !== userDna.child1) return false; - break; - case 2: - if (staffDna.root !== userDna.root) return false; - if (staffDna.child1 !== userDna.child1) return false; - if (staffDna.child2 !== userDna.child2) return false; - break; - case 3: - if (staffDna.root !== userDna.root) return false; - if (staffDna.child1 !== userDna.child1) return false; - if (staffDna.child2 !== userDna.child2) return false; - if (staffDna.child3 !== userDna.child3) return false; - break; - case 4: - if (staffDna.root !== userDna.root) return false; - if (staffDna.child1 !== userDna.child1) return false; - if (staffDna.child2 !== userDna.child2) return false; - if (staffDna.child3 !== userDna.child3) return false; - if (staffDna.child4 !== userDna.child4) return false; - break; - } - return true; - } - - // BROTHER: Staff เห็น User ที่อยู่ในกิ่งข้างบนและลูก - if (privilege === "BROTHER") { - if (userNode < staffNode - 1 || userNode > 4) return false; - - if (staffNode === 0 || staffNode === 1) { - if (staffDna.root !== userDna.root) return false; - } /*else if (staffNode === 1) { - if (staffDna.root !== userDna.root) return false; - if (staffDna.child1 !== userDna.child1) return false; - }*/ else if (staffNode === 2) { - if (staffDna.child1 !== userDna.child1) return false; - // if (staffDna.child2 !== userDna.child2 && userDna.child2 !== null) return false; - } else if (staffNode === 3) { - if (staffDna.child2 !== userDna.child2) return false; - // if (staffDna.child3 !== userDna.child3 && userDna.child3 !== null) return false; - } else if (staffNode === 4) { - if (staffDna.child3 !== userDna.child3) return false; - // if (staffDna.child4 !== userDna.child4 && userDna.child4 !== null) return false; - } - return true; - } - // กรณีอื่นๆ ให้ผ่าน - return true; - }); - return new HttpSuccess(filteredPosMasters); + return new HttpSuccess(posMasters); } } diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 44747b09..801d4b97 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -15,8 +15,6 @@ import permission from "../interfaces/permission"; import { ProfileEmployee } from "../entities/ProfileEmployee"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { OrgRevision } from "../entities/OrgRevision"; -import { PosMasterAct } from "../entities/PosMasterAct"; -import { actingPositionService } from "../services/ActingPositionService"; const REDIS_HOST = process.env.REDIS_HOST; const REDIS_PORT = process.env.REDIS_PORT; @@ -32,18 +30,15 @@ export class PermissionController extends Controller { private authRoleAttrRepo = AppDataSource.getRepository(AuthRoleAttr); private authSysRepo = AppDataSource.getRepository(AuthSys); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); - private posMasterActRepo = AppDataSource.getRepository(PosMasterAct); private redis = require("redis"); @Get("") public async getPermission(@Request() request: RequestWithUser) { - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ select: ["id"], @@ -59,25 +54,17 @@ export class PermissionController extends Controller { } } - // Query ตำแหน่งรักษาการโดยใช้ service ที่มีอยู่ - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - 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 { + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); let posMaster: any = await this.posMasterRepository.findOne({ select: ["authRoleId"], where: { @@ -93,190 +80,41 @@ export class PermissionController extends Controller { orgRevisionId: orgRevision?.id, }, }); - } - - // ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position) - if (!posMaster && !actingData.isAct) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); - } - - let getDetail: any = null; - let roleAttrData: any[] = []; - - if (posMaster) { - getDetail = await this.authRoleRepo.findOne({ - select: ["id", "roleName", "roleDescription"], - where: { id: posMaster.authRoleId }, - }); - - if (!getDetail) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (!posMaster) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); } + } + const getDetail = await this.authRoleRepo.findOne({ + select: ["id", "roleName", "roleDescription"], + where: { id: posMaster.authRoleId }, + }); - roleAttrData = await this.authRoleAttrRepo.find({ - select: [ - "authSysId", - "parentNode", - "attrOwnership", - "attrIsCreate", - "attrIsList", - "attrIsGet", - "attrIsUpdate", - "attrIsDelete", - "attrPrivilege", - ], - where: { authRoleId: getDetail.id }, - }); - } else { - // ถ้าไม่มี posMaster แต่มี acting: สร้าง getDetail เปล่าๆ - getDetail = { - id: null, - roleName: "Acting", - roleDescription: "สิทธิ์จากตำแหน่งรักษาการ", - }; + if (!getDetail) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); } - // ถ้า 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(); + const roleAttrData = await this.authRoleAttrRepo.find({ + select: [ + "authSysId", + "parentNode", + "attrOwnership", + "attrIsCreate", + "attrIsList", + "attrIsGet", + "attrIsUpdate", + "attrIsDelete", + "attrPrivilege", + ], + where: { authRoleId: getDetail.id }, + }); - // ดึง 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(); - - // ลำดับความสำคัญของ privilege (มากไปน้อย) - const privilegePriority: Record = { - "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 = { - ...getDetail, - roles: roleAttrData, - isActing: false, - }; - } + reply = { + ...getDetail, + roles: roleAttrData, + }; redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); } return new HttpSuccess(reply); - } finally { - if (redisClient) { - redisClient.quit(); - } - } } @Get("menu") @@ -288,13 +126,11 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -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); if (reply != null) { reply = JSON.parse(reply); @@ -338,71 +167,27 @@ export class PermissionController extends Controller { orgRevisionId: orgRevision?.id, }, }); + if (!posMaster) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); + } } - // ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position) - if (!posMaster && !actingData.isAct) { + if (!posMaster.authRoleId) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); } - let authRole: any = null; - let roleAttrData: any[] = []; + const authRole = await this.authRoleRepo.findOne({ + select: ["id"], + where: { id: posMaster.authRoleId }, + }); - if (posMaster) { - if (!posMaster.authRoleId) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); - } - - authRole = await this.authRoleRepo.findOne({ - select: ["id"], - where: { id: posMaster.authRoleId }, - }); - - if (!authRole) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); - } - - // ดึง roleAttrData ของ user ปกติ - roleAttrData = await this.authRoleAttrRepo.find({ - select: ["authSysId", "parentNode"], - where: { authRoleId: authRole.id, attrIsList: true }, - }); + if (!authRole) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); } - - // ถ้ามี 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 roleAttrData = await this.authRoleAttrRepo.find({ + select: ["authSysId", "parentNode"], + where: { authRoleId: authRole.id, attrIsList: true }, + }); const parentNode = roleAttrData.map((x) => x.parentNode); const authSysId = roleAttrData.map((x) => x.authSysId); const sysId = parentNode.concat(authSysId); @@ -447,112 +232,6 @@ export class PermissionController extends Controller { } 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); } - /** - * API permission with acting positions - * @summary permission with acting positions (dotnet api) - * @param {string} action action - * @param {string} system authSysId - */ - @Get("dotnet-acting/{action}/{system}") - public async dotnetActing( - @Request() req: RequestWithUser, - @Path() action: string, - @Path() system: string, - ) { - if (!["CREATE", "DELETE", "GET", "LIST", "UPDATE"].includes(action)) { - throw new HttpError(HttpStatus.NOT_FOUND, "Action ไม่ถูกต้อง"); - } - // ดึง privilege ตามปกติ - let privilege = await new permission().Permission(req, system.toLocaleUpperCase(), action); - - // ดึงข้อมูล profile และ orgRevision - let profile: any = await this.profileRepo.findOne({ - select: ["id"], - where: { keycloak: req.user.sub }, - }); - - if (!profile) { - profile = await this.profileEmployeeRepo.findOne({ - select: ["id"], - where: { keycloak: req.user.sub }, - }); - if (!profile) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); - } - } - - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }); - - // ดึงข้อมูลตำแหน่งที่รักษาการ - const actingData = await actingPositionService.getActingPositionsWithPrivilege( - profile.id, - orgRevision?.id, - action, - system.toLocaleUpperCase() - ); - - // ส่งค่ากลับเหมือน dotnet endpoint แต่เพิ่ม isAct และ posMasterActs - return new HttpSuccess({ - privilege, - isAct: actingData.isAct, - posMasterActs: actingData.posMasterActs, - }); - } - /** * API permission (dotnet api) * @summary permission (dotnet api) @@ -686,13 +307,11 @@ export class PermissionController extends Controller { @Path() system: string, @Path() action: string, ) { - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -781,11 +400,6 @@ export class PermissionController extends Controller { } return new HttpSuccess(reply); - } finally { - if (redisClient) { - redisClient.quit(); - } - } } @Get("user/{system}/{action}/{id}") @@ -802,13 +416,11 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let org = this.PermissionOrg(request, system, action); let reply = await getAsync("user_" + id); @@ -889,21 +501,14 @@ export class PermissionController extends Controller { } return new HttpSuccess(reply); - } finally { - if (redisClient) { - redisClient.quit(); - } - } } public async getPermissionFunc(@Request() request: RequestWithUser) { - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ select: ["id"], @@ -919,25 +524,17 @@ export class PermissionController extends Controller { } } - // Query ตำแหน่งรักษาการ - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - 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 { + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); let posMaster: any = await this.posMasterRepository.findOne({ select: ["authRoleId"], where: { @@ -953,178 +550,41 @@ export class PermissionController extends Controller { orgRevisionId: orgRevision?.id, }, }); - } - - // ตรวจสอบว่ามีสิทธิ์อย่างน้อยหนึ่งอย่าง (posMaster หรือ acting position) - if (!posMaster && !actingData.isAct) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); - } - - let getDetail: any = null; - let roleAttrData: any[] = []; - - if (posMaster) { - getDetail = await this.authRoleRepo.findOne({ - select: ["id", "roleName", "roleDescription"], - where: { id: posMaster.authRoleId }, - }); - if (!getDetail) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + if (!posMaster) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลสิทธิ์"); } - - roleAttrData = await this.authRoleAttrRepo.find({ - select: [ - "authSysId", - "parentNode", - "attrOwnership", - "attrIsCreate", - "attrIsList", - "attrIsGet", - "attrIsUpdate", - "attrIsDelete", - "attrPrivilege", - ], - 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 = { - "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(); - - 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 = { - ...getDetail, - roles: roleAttrData, - }; + const getDetail = await this.authRoleRepo.findOne({ + select: ["id", "roleName", "roleDescription"], + where: { id: posMaster.authRoleId }, + }); + if (!getDetail) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); } + const roleAttrData = await this.authRoleAttrRepo.find({ + select: [ + "authSysId", + "parentNode", + "attrOwnership", + "attrIsCreate", + "attrIsList", + "attrIsGet", + "attrIsUpdate", + "attrIsDelete", + "attrPrivilege", + ], + where: { authRoleId: getDetail.id }, + }); + + reply = { + ...getDetail, + roles: roleAttrData, + }; redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); } return reply; - } finally { - if (redisClient) { - redisClient.quit(); - } - } } public async Permission(req: RequestWithUser, system: string, action: string) { @@ -1150,13 +610,11 @@ export class PermissionController extends Controller { } public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) { - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -1174,159 +632,75 @@ export class PermissionController extends Controller { } } - // Query ตำแหน่งรักษาการ - const orgRevision = await this.orgRevisionRepository.findOne({ - select: ["id"], - where: { - orgRevisionIsDraft: false, - 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); + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); + if (profileType == "OFFICER") { + const posMaster = await this.posMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: orgRevision?.id, + }, + }); + if (!posMaster) { reply = { - orgRootId: actingOrgData.orgRootId, - orgChild1Id: actingOrgData.orgChild1Id, - orgChild2Id: actingOrgData.orgChild2Id, - orgChild3Id: actingOrgData.orgChild3Id, - orgChild4Id: actingOrgData.orgChild4Id, + orgRootId: null, + orgChild1Id: null, + orgChild2Id: null, + orgChild3Id: null, + orgChild4Id: null, privilege: privilege, }; } else { - // ระบบนี้มาจากตำแหน่งปกติ ใช้ org ปกติ - reply = await this.getBaseOrgScope(profile.id, orgRevision?.id, profileType, privilege); + reply = { + orgRootId: posMaster.orgRootId, + orgChild1Id: posMaster.orgChild1Id, + orgChild2Id: posMaster.orgChild2Id, + orgChild3Id: posMaster.orgChild3Id, + orgChild4Id: posMaster.orgChild4Id, + privilege: privilege, + }; } + redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply)); } else { - // ไม่มี acting ใช้ org ปกติ - reply = await this.getBaseOrgScope(profile.id, orgRevision?.id, profileType, privilege); + const posMaster = await this.posMasterEmpRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: orgRevision?.id, + }, + }); + if (!posMaster) { + reply = { + orgRootId: null, + orgChild1Id: null, + orgChild2Id: null, + orgChild3Id: null, + orgChild4Id: null, + privilege: privilege, + }; + } else { + reply = { + orgRootId: posMaster.orgRootId, + orgChild1Id: posMaster.orgChild1Id, + orgChild2Id: posMaster.orgChild2Id, + orgChild3Id: posMaster.orgChild3Id, + orgChild4Id: posMaster.orgChild4Id, + privilege: privilege, + }; + } + redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply)); } - - 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") { - const posMaster = await this.posMasterRepository.findOne({ - where: { - current_holderId: profileId, - orgRevisionId: orgRevisionId, - }, - }); - if (!posMaster) { - return { - orgRootId: null, - orgChild1Id: null, - orgChild2Id: null, - orgChild3Id: null, - orgChild4Id: null, - privilege: privilege, - }; - } else { - return { - orgRootId: posMaster.orgRootId, - orgChild1Id: posMaster.orgChild1Id, - orgChild2Id: posMaster.orgChild2Id, - orgChild3Id: posMaster.orgChild3Id, - orgChild4Id: posMaster.orgChild4Id, - privilege: privilege, - }; - } - } else { - const posMaster = await this.posMasterEmpRepository.findOne({ - where: { - current_holderId: profileId, - orgRevisionId: orgRevisionId, - }, - }); - if (!posMaster) { - return { - orgRootId: null, - orgChild1Id: null, - orgChild2Id: null, - orgChild3Id: null, - orgChild4Id: null, - privilege: privilege, - }; - } else { - return { - orgRootId: posMaster.orgRootId, - orgChild1Id: posMaster.orgChild1Id, - orgChild2Id: posMaster.orgChild2Id, - orgChild3Id: posMaster.orgChild3Id, - orgChild4Id: posMaster.orgChild4Id, - privilege: privilege, - }; - } - } - } - - // 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) { @@ -1408,13 +782,11 @@ export class PermissionController extends Controller { @Get("checkOrg/{keycloakId}") public async checkOrg(@Path() keycloakId: string) { - let redisClient; - try { - redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - // const getAsync = promisify(redisClient.get).bind(redisClient); + const redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + // const getAsync = promisify(redisClient.get).bind(redisClient); // let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -1492,10 +864,5 @@ export class PermissionController extends Controller { // } return new HttpSuccess(reply); - } finally { - if (redisClient) { - redisClient.quit(); - } - } } } diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index fbc09201..dd4acd1b 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -24,10 +24,6 @@ import Extension from "../interfaces/extension"; import { ProfileActposition } from "../entities/ProfileActposition"; import { RequestWithUser } from "../middlewares/user"; 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") @Tags("PosMasterAct") @@ -41,7 +37,6 @@ export class PosMasterActController extends Controller { private posMasterActRepository = AppDataSource.getRepository(PosMasterAct); private posMasterRepository = AppDataSource.getRepository(PosMaster); private actpositionRepository = AppDataSource.getRepository(ProfileActposition); - private redis = require("redis"); /** * API เพิ่มรักษาการในตำแหน่ง @@ -97,6 +92,7 @@ export class PosMasterActController extends Controller { return new HttpSuccess(posMasterAct); } + /** * API ค้นหาตำแหน่งในระบบสมัครสอบ ขรก. * @@ -129,7 +125,9 @@ export class PosMasterActController extends Controller { 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); const query = await AppDataSource.getRepository(PosMaster) @@ -174,31 +172,31 @@ export class PosMasterActController extends Controller { posMasterMain.orgRootId == null ? "posMaster.orgRootId IS NULL" : "posMaster.orgRootId = :orgRootId", - { orgRootId: posMasterMain.orgRootId }, + { orgRootId: posMasterMain.orgRootId } ) .andWhere( posMasterMain.orgChild1Id == null ? "posMaster.orgChild1Id IS NULL" : "posMaster.orgChild1Id = :orgChild1Id", - { orgChild1Id: posMasterMain.orgChild1Id }, + { orgChild1Id: posMasterMain.orgChild1Id } ) .andWhere( posMasterMain.orgChild2Id == null ? "posMaster.orgChild2Id IS NULL" : "posMaster.orgChild2Id = :orgChild2Id", - { orgChild2Id: posMasterMain.orgChild2Id }, + { orgChild2Id: posMasterMain.orgChild2Id } ) .andWhere( posMasterMain.orgChild3Id == null ? "posMaster.orgChild3Id IS NULL" : "posMaster.orgChild3Id = :orgChild3Id", - { orgChild3Id: posMasterMain.orgChild3Id }, + { orgChild3Id: posMasterMain.orgChild3Id } ) .andWhere( posMasterMain.orgChild4Id == null ? "posMaster.orgChild4Id IS NULL" : "posMaster.orgChild4Id = :orgChild4Id", - { orgChild4Id: posMasterMain.orgChild4Id }, + { orgChild4Id: posMasterMain.orgChild4Id } ); } } else { @@ -212,7 +210,7 @@ export class PosMasterActController extends Controller { new Brackets((qb) => { qb.where( `CONCAT(current_holder.prefix, current_holder.firstName, ' ', current_holder.lastName) LIKE :keyword`, - { keyword: `%${keyword}%` }, + { keyword: `%${keyword}%` } ) .orWhere(`current_holder.citizenId LIKE :keyword`, { keyword: `%${keyword}%`, @@ -230,7 +228,7 @@ export class PosMasterActController extends Controller { ' ', posMaster.posMasterNo ) LIKE :keyword`, - { keyword: `%${keyword}%` }, + { keyword: `%${keyword}%` } ) .orWhere(`posLevel.posLevelName LIKE :keyword`, { keyword: `%${keyword}%`, @@ -240,8 +238,8 @@ export class PosMasterActController extends Controller { }) .orWhere(`current_holder.position LIKE :keyword`, { keyword: `%${keyword}%`, - }); - }), + }) + }) ); } @@ -282,6 +280,7 @@ export class PosMasterActController extends Controller { return new HttpSuccess({ data: data, total }); } + /** * API ลบรักษาการในตำแหน่ง * @@ -296,7 +295,6 @@ export class PosMasterActController extends Controller { where: { id: id, }, - relations: ["posMasterChild", "posMasterChild.current_holder"], }); try { result = await this.posMasterActRepository.delete({ id: id }); @@ -321,22 +319,6 @@ export class PosMasterActController extends Controller { await this.posMasterActRepository.save(p); }); } - - // ลบ Redis cache ของคนที่เป็น acting - if (posMasterAct != null && posMasterAct.posMasterChild?.current_holderId) { - const profileId = posMasterAct.posMasterChild.current_holderId; - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - - const delAsync = promisify(redisClient.del).bind(redisClient); - await delAsync("role_" + profileId); - await delAsync("menu_" + profileId); - - redisClient.quit(); - } - return new HttpSuccess(); } @@ -708,12 +690,12 @@ export class PosMasterActController extends Controller { x.posMasterChild?.orgRoot?.orgRootShortName, ].find((name) => !!name) && x.posMasterChild?.posMasterNo ? `${[ - x.posMasterChild?.orgChild4?.orgChild4ShortName, - x.posMasterChild?.orgChild3?.orgChild3ShortName, - x.posMasterChild?.orgChild2?.orgChild2ShortName, - x.posMasterChild?.orgChild1?.orgChild1ShortName, - x.posMasterChild?.orgRoot?.orgRootShortName, - ].find((name) => !!name)} ${x.posMasterChild.posMasterNo}` + x.posMasterChild?.orgChild4?.orgChild4ShortName, + x.posMasterChild?.orgChild3?.orgChild3ShortName, + x.posMasterChild?.orgChild2?.orgChild2ShortName, + x.posMasterChild?.orgChild1?.orgChild1ShortName, + x.posMasterChild?.orgRoot?.orgRootShortName, + ].find((name) => !!name)} ${x.posMasterChild.posMasterNo}` : x.posMasterChild?.posMasterNo || null; const orgShortNameAct = [ @@ -786,9 +768,6 @@ export class PosMasterActController extends Controller { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลรักษาการในตำแหน่งของหน่วยงานนี้"); } - // เก็บรวบรวม profileIds ทั้งหมดเพื่อ clear cache หลังจากบันทึกเสร็จ - const profileIdsToClearCache = new Set(); - await Promise.all( posMasterActs.map(async (posMasterAct) => { const orgShortName = @@ -803,8 +782,6 @@ export class PosMasterActController extends Controller { const profileId = posMasterAct.posMasterChild?.current_holderId; if (profileId) { - profileIdsToClearCache.add(profileId); - const existingActivePositions = await this.actpositionRepository.find({ select: [ "id", @@ -813,7 +790,7 @@ export class PosMasterActController extends Controller { "lastUpdateFullName", "lastUpdatedAt", "dateEnd", - "isDeleted", + "isDeleted" ], 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(); } } diff --git a/src/controllers/PositionController.ts b/src/controllers/PositionController.ts index efcd1497..75d6c2a0 100644 --- a/src/controllers/PositionController.ts +++ b/src/controllers/PositionController.ts @@ -39,7 +39,6 @@ import { AuthRole } from "../entities/AuthRole"; import { RequestWithUser } from "../middlewares/user"; import permission from "../interfaces/permission"; import { resolveNodeLevel, setLogDataDiff } from "../interfaces/utils"; -import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import { PosMasterAssign } from "../entities/PosMasterAssign"; import { Assign } from "../entities/Assign"; import { ProfileEmployee } from "../entities/ProfileEmployee"; @@ -1257,15 +1256,7 @@ export class PositionController extends Controller { ) { await new permission().PermissionUpdate(request, "SYS_ORG"); const posMaster = await this.posMasterRepository.findOne({ - relations: [ - "positions", - "orgRevision", - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - ], + relations: ["positions", "orgRevision"], where: { id: id }, }); if (!posMaster) { @@ -1460,24 +1451,10 @@ export class PositionController extends Controller { }), ); - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - if (posMaster.orgRevision?.orgRevisionIsCurrent == true && posMaster.current_holderId) { - const _profile = await this.profileRepository.findOne({ - where: { id: posMaster.current_holderId }, - }); - if (_profile) { - _profile.posMasterNo = getPosMasterNo(posMaster); - _profile.org = getOrgFullName(posMaster); - await this.profileRepository.save(_profile); - } - } // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (posMaster.orgRevision?.orgRevisionIsCurrent == true && !posMaster.isSit) { const _position = requestBody.positions.find((p) => p.positionIsSelected == true); if (_position) { - const _posExecutive = _position.posExecutiveId - ? await this.posExecutiveRepository.findOne({ where: { id: _position.posExecutiveId } }) - : null; const current_holderId: any = posMaster.current_holderId; const _profile = await this.profileRepository.findOne({ where: { id: current_holderId }, @@ -1486,10 +1463,6 @@ export class PositionController extends Controller { _profile.position = _position.posDictName ?? _null; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; - _profile.positionField = _position.posDictField ?? _null; - _profile.posExecutive = _posExecutive?.posExecutiveName ?? _null; - _profile.positionArea = _position.posDictArea ?? _null; - _profile.positionExecutiveField = _position.posDictExecutiveField ?? _null; await this.profileRepository.save(_profile); } } @@ -2154,11 +2127,11 @@ export class PositionController extends Controller { let checkChildConditions: any = {}; let keywordAsInt: any; let searchShortName = "1=1"; - let searchShortName0 = `CONCAT_WS(" ",orgRoot.orgRootShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName1 = `CONCAT_WS(" ",orgChild1.orgChild1ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName2 = `CONCAT_WS(" ",orgChild2.orgChild2ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName3 = `CONCAT_WS(" ",orgChild3.orgChild3ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; - let searchShortName4 = `CONCAT_WS(" ",orgChild4.orgChild4ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix)`; + let searchShortName0 = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo)`; + let searchShortName1 = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo)`; + let searchShortName2 = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo)`; + let searchShortName3 = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo)`; + let searchShortName4 = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo)`; let _data = await new permission().PermissionOrgList(request, "SYS_ORG"); if (body.type === 0) { typeCondition = { @@ -2168,7 +2141,7 @@ export class PositionController extends Controller { checkChildConditions = { orgChild1Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgRoot.orgRootShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgRoot.orgRootShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } } else if (body.type === 1) { typeCondition = { @@ -2178,7 +2151,7 @@ export class PositionController extends Controller { checkChildConditions = { orgChild2Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgChild1.orgChild1ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } } else if (body.type === 2) { typeCondition = { @@ -2188,7 +2161,7 @@ export class PositionController extends Controller { checkChildConditions = { orgChild3Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgChild2.orgChild2ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } } else if (body.type === 3) { typeCondition = { @@ -2198,13 +2171,13 @@ export class PositionController extends Controller { checkChildConditions = { orgChild4Id: IsNull(), }; - searchShortName = `CONCAT_WS(" ",orgChild3.orgChild3ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } } else if (body.type === 4) { typeCondition = { orgChild4Id: body.id, }; - searchShortName = `CONCAT_WS(" ",orgChild4.orgChild4ShortName,posMaster.posMasterNoPrefix,posMaster.posMasterNo,posMaster.posMasterNoSuffix) like '%${body.keyword}%'`; + searchShortName = `CONCAT(orgChild4.orgChild4ShortName," ",posMaster.posMasterNo) like '%${body.keyword}%'`; } let findPosition: any; let masterId = new Array(); @@ -2414,16 +2387,16 @@ export class PositionController extends Controller { ? "posMaster.orgRootId IN (:...root)" : "posMaster.orgRootId is null" : "1=1", - { root: _data.root }, + { root: _data.root } ) .andWhere( _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? "posMaster.orgChild1Id IN (:...child1)" - : // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `posMaster.orgChild1Id is null` + // : `posMaster.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `posMaster.orgChild1Id is null` : "1=1", - { child1: _data.child1 }, + { child1: _data.child1 } ) .andWhere( _data.child2 != undefined && _data.child2 != null @@ -2454,27 +2427,26 @@ export class PositionController extends Controller { { child4: _data.child4, }, - ); + ) // .andWhere(checkChildConditions) // .andWhere(typeCondition) // .andWhere(revisionCondition); if (body.keyword != null && body.keyword != "") { - query - .orWhere( - new Brackets((qb) => { - qb.andWhere( - body.keyword != null && body.keyword != "" - ? body.isAll == false - ? searchShortName - : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` - : "1=1", - ) - .andWhere(checkChildConditions) - .andWhere(typeCondition) - .andWhere(revisionCondition); - }), - ) + query.orWhere( + new Brackets((qb) => { + qb.andWhere( + body.keyword != null && body.keyword != "" + ? body.isAll == false + ? searchShortName + : `CASE WHEN posMaster.orgChild1 is null THEN ${searchShortName0} WHEN posMaster.orgChild2 is null THEN ${searchShortName1} WHEN posMaster.orgChild3 is null THEN ${searchShortName2} WHEN posMaster.orgChild4 is null THEN ${searchShortName3} ELSE ${searchShortName4} END LIKE '%${body.keyword}%'` + : "1=1", + ) + .andWhere(checkChildConditions) + .andWhere(typeCondition) + .andWhere(revisionCondition); + }), + ) .orWhere( new Brackets((qb) => { qb.andWhere( @@ -2760,19 +2732,7 @@ export class PositionController extends Controller { id: data.id, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, })); - // Bulk update using CASE WHEN instead of save() per row - const caseClauses_0 = sortData_0 - .map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`) - .join(" "); - const ids_0 = sortData_0.map((d) => `'${d.id}'`).join(","); - await this.posMasterRepository - .createQueryBuilder() - .update(PosMaster) - .set({ - posMasterOrder: () => `CASE id ${caseClauses_0} END`, - }) - .where(`id IN (${ids_0})`) - .execute(); + await this.posMasterRepository.save(sortData_0, { data: request }); setLogDataDiff(request, { before, after: sortData_0 }); break; } @@ -2801,19 +2761,7 @@ export class PositionController extends Controller { id: data.id, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, })); - // Bulk update using CASE WHEN instead of save() per row - const caseClauses_1 = sortData_1 - .map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`) - .join(" "); - const ids_1 = sortData_1.map((d) => `'${d.id}'`).join(","); - await this.posMasterRepository - .createQueryBuilder() - .update(PosMaster) - .set({ - posMasterOrder: () => `CASE id ${caseClauses_1} END`, - }) - .where(`id IN (${ids_1})`) - .execute(); + await this.posMasterRepository.save(sortData_1, { data: request }); setLogDataDiff(request, { before, after: sortData_1 }); break; } @@ -2842,19 +2790,7 @@ export class PositionController extends Controller { id: data.id, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, })); - // Bulk update using CASE WHEN instead of save() per row - const caseClauses_2 = sortData_2 - .map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`) - .join(" "); - const ids_2 = sortData_2.map((d) => `'${d.id}'`).join(","); - await this.posMasterRepository - .createQueryBuilder() - .update(PosMaster) - .set({ - posMasterOrder: () => `CASE id ${caseClauses_2} END`, - }) - .where(`id IN (${ids_2})`) - .execute(); + await this.posMasterRepository.save(sortData_2, { data: request }); setLogDataDiff(request, { before, after: sortData_2 }); break; } @@ -2883,19 +2819,7 @@ export class PositionController extends Controller { id: data.id, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, })); - // Bulk update using CASE WHEN instead of save() per row - const caseClauses_3 = sortData_3 - .map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`) - .join(" "); - const ids_3 = sortData_3.map((d) => `'${d.id}'`).join(","); - await this.posMasterRepository - .createQueryBuilder() - .update(PosMaster) - .set({ - posMasterOrder: () => `CASE id ${caseClauses_3} END`, - }) - .where(`id IN (${ids_3})`) - .execute(); + await this.posMasterRepository.save(sortData_3, { data: request }); setLogDataDiff(request, { before, after: sortData_3 }); break; } @@ -2924,19 +2848,7 @@ export class PositionController extends Controller { id: data.id, posMasterOrder: requestBody.sortId.indexOf(data.id) + 1, })); - // Bulk update using CASE WHEN instead of save() per row - const caseClauses_4 = sortData_4 - .map((d) => `WHEN '${d.id}' THEN ${d.posMasterOrder}`) - .join(" "); - const ids_4 = sortData_4.map((d) => `'${d.id}'`).join(","); - await this.posMasterRepository - .createQueryBuilder() - .update(PosMaster) - .set({ - posMasterOrder: () => `CASE id ${caseClauses_4} END`, - }) - .where(`id IN (${ids_4})`) - .execute(); + await this.posMasterRepository.save(sortData_4, { data: request }); setLogDataDiff(request, { before, after: sortData_4 }); break; } @@ -3043,50 +2955,50 @@ export class PositionController extends Controller { const type0LastPosMasterNo = requestBody.type == 0 ? await this.posMasterRepository.find({ - where: { - orgRootId: requestBody.id, - orgChild1Id: IsNull(), - }, - }) + where: { + orgRootId: requestBody.id, + orgChild1Id: IsNull(), + }, + }) : []; const type1LastPosMasterNo = requestBody.type == 1 ? await this.posMasterRepository.find({ - where: { - orgChild1Id: requestBody.id, - orgChild2Id: IsNull(), - }, - }) + where: { + orgChild1Id: requestBody.id, + orgChild2Id: IsNull(), + }, + }) : []; const type2LastPosMasterNo = requestBody.type == 2 ? await this.posMasterRepository.find({ - where: { - orgChild2Id: requestBody.id, - orgChild3Id: IsNull(), - }, - }) + where: { + orgChild2Id: requestBody.id, + orgChild3Id: IsNull(), + }, + }) : []; const type3LastPosMasterNo = requestBody.type == 3 ? await this.posMasterRepository.find({ - where: { - orgChild3Id: requestBody.id, - orgChild4Id: IsNull(), - }, - }) + where: { + orgChild3Id: requestBody.id, + orgChild4Id: IsNull(), + }, + }) : []; const type4LastPosMasterNo = requestBody.type == 4 ? await this.posMasterRepository.find({ - where: { - orgChild4Id: requestBody.id, - }, - }) + where: { + orgChild4Id: requestBody.id, + }, + }) : []; const allLastPosMasterNo = [ @@ -3415,52 +3327,6 @@ export class PositionController extends Controller { posMaster.lastUpdatedAt = new Date(); await this.posMasterRepository.save(posMaster, { data: request }); 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); - } - } - } } }), ); @@ -3927,7 +3793,7 @@ export class PositionController extends Controller { await new permission().PermissionUpdate(request, "SYS_ORG"); const dataMaster = await this.posMasterRepository.findOne({ where: { id: requestBody.posMaster }, - relations: ["positions", "orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], + relations: ["positions"], }); if (!dataMaster) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); @@ -3959,24 +3825,16 @@ export class PositionController extends Controller { if (_profile) { let _position = await this.positionRepository.findOne({ where: { id: requestBody.position, posMasterId: requestBody.posMaster }, - relations: ["posExecutive"], }); if (_position) { - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - _profile.posMasterNo = getPosMasterNo(dataMaster); - _profile.org = getOrgFullName(dataMaster); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (!dataMaster.isSit) { + if(!dataMaster.isSit){ _profile.position = _position.positionName; _profile.posTypeId = _position.posTypeId; _profile.posLevelId = _position.posLevelId; - _profile.positionField = _position.positionField ?? _null; - _profile.posExecutive = _position.posExecutive?.posExecutiveName ?? _null; - _profile.positionArea = _position.positionArea ?? _null; - _profile.positionExecutiveField = _position.positionExecutiveField ?? _null; + await this.profileRepository.save(_profile); + setLogDataDiff(request, { before, after: _profile }); } - await this.profileRepository.save(_profile); - setLogDataDiff(request, { before, after: _profile }); } } dataMaster.current_holderId = requestBody.profileId; @@ -4003,7 +3861,7 @@ export class PositionController extends Controller { */ @Post("profile/delete/{id}") 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({ where: { id: id }, relations: ["positions", "orgRevision"], @@ -5311,9 +5169,9 @@ export class PositionController extends Controller { } /** - * API รายการตำแหน่งติดเงื่อนไข + * API รายการอัตรากำลัง * - * @summary รายการตำแหน่งติดเงื่อนไข + * @summary ORG_070 - รายการอัตรากำลัง (ADMIN) #56 * */ @Post("master/position-condition") @@ -5324,7 +5182,7 @@ export class PositionController extends Controller { id: string; revisionId: string; type: number; - isAll: boolean; // true คือเลือกเฉพาะตำแหน่งติดเงื่อนไข / false คือเลือกตำแหน่งทั้งหมด + isAll: boolean; page: number; pageSize: number; keyword?: string; @@ -5344,7 +5202,7 @@ export class PositionController extends Controller { let level: any = resolveNodeLevel(orgDna); const cannotViewRootPosMaster = - _data.privilege === "PARENT" || + (_data.privilege === "PARENT") || (_data.privilege === "BROTHER" && level > 1) || (_data.privilege === "CHILD" && level > 0) || (_data.privilege === "NORMAL" && level != 0); @@ -5376,46 +5234,46 @@ export class PositionController extends Controller { typeCondition = { ...(cannotViewRootPosMaster ? { orgRootId: null } : { orgRootId: body.id }), }; - // if (!body.isAll) { - // checkChildConditions = { - // orgChild1Id: IsNull(), - // }; - // searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - // } else { - // } + if (!body.isAll) { + checkChildConditions = { + orgChild1Id: IsNull(), + }; + searchShortName = `CONCAT(orgRoot.orgRootShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + } else { + } } else if (body.type === 1) { typeCondition = { ...(cannotViewChild1PosMaster ? { orgChild1Id: null } : { orgChild1Id: body.id }), }; - // if (!body.isAll) { - // checkChildConditions = { - // orgChild2Id: IsNull(), - // }; - // searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - // } else { - // } + if (!body.isAll) { + checkChildConditions = { + orgChild2Id: IsNull(), + }; + searchShortName = `CONCAT(orgChild1.orgChild1ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + } else { + } } else if (body.type === 2) { typeCondition = { ...(cannotViewChild2PosMaster ? { orgChild2Id: null } : { orgChild2Id: body.id }), }; - // if (!body.isAll) { - // checkChildConditions = { - // orgChild3Id: IsNull(), - // }; - // searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - // } else { - // } + if (!body.isAll) { + checkChildConditions = { + orgChild3Id: IsNull(), + }; + searchShortName = `CONCAT(orgChild2.orgChild2ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + } else { + } } else if (body.type === 3) { typeCondition = { ...(cannotViewChild3PosMaster ? { orgChild3Id: null } : { orgChild3Id: body.id }), }; - // if (!body.isAll) { - // checkChildConditions = { - // orgChild4Id: IsNull(), - // }; - // searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; - // } else { - // } + if (!body.isAll) { + checkChildConditions = { + orgChild4Id: IsNull(), + }; + searchShortName = `CONCAT(orgChild3.orgChild3ShortName," ",COALESCE(posMaster.posMasterNoPrefix, ""),posMaster.posMasterNo,COALESCE(posMaster.posMasterNoSuffix, "")) like '%${body.keyword}%'`; + } else { + } } else if (body.type === 4) { typeCondition = { ...(cannotViewChild4PosMaster ? { orgChild4Id: null } : { orgChild4Id: body.id }), @@ -5488,7 +5346,7 @@ export class PositionController extends Controller { (masterId.length > 0 ? { id: In(masterId) } : { posMasterNo: Like(`%${body.keyword}%`) })), - ...(!body.isAll && { isCondition: true }), + current_holderId: IsNull(), }, ]; let [posMaster, total] = await AppDataSource.getRepository(PosMaster) @@ -5557,15 +5415,15 @@ export class PositionController extends Controller { new Brackets((qb) => { qb.andWhere( 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", ) .andWhere(checkChildConditions) .andWhere(typeCondition) - .andWhere(revisionCondition); - if (!body.isAll) { - qb.andWhere({ isCondition: true }); - } + .andWhere(revisionCondition) + .andWhere({ current_holderId: IsNull() }); }), ) .orWhere( @@ -5575,10 +5433,8 @@ export class PositionController extends Controller { ) .andWhere(checkChildConditions) .andWhere(typeCondition) - .andWhere(revisionCondition); - if (!body.isAll) { - qb.andWhere({ isCondition: true }); - } + .andWhere(revisionCondition) + .andWhere({ current_holderId: IsNull() }); }), ) .orderBy("orgRoot.orgRootOrder", "ASC") diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 0e5abf92..9e758e67 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -89,9 +89,9 @@ import { ProfileAssistance } from "../entities/ProfileAssistance"; import { CommandRecive } from "../entities/CommandRecive"; import { CommandCode } from "../entities/CommandCode"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; -import { CreatePosMasterHistoryOfficer, getTopDegrees, getPosMasterPositions } from "../services/PositionService"; +import { CreatePosMasterHistoryOfficer, getTopDegrees } from "../services/PositionService"; import { ProfileLeaveService } from "../services/ProfileLeaveService"; -// import { PostRetireToExprofile } from "./ExRetirementController"; +import { PostRetireToExprofile } from "./ExRetirementController"; import { getPosNumCodeSit } from "../services/CommandService"; @Route("api/v1/org/profile") @Tags("Profile") @@ -204,7 +204,7 @@ export class ProfileController extends Controller { }, }); ImgUrl = response_.data.downloadUrl; - } catch { } + } catch {} } const province = await this.provinceRepository.findOneBy({ id: profile.registrationProvinceId, @@ -216,36 +216,36 @@ export class ProfileController extends Controller { const root = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4; @@ -293,38 +293,38 @@ export class ProfileController extends Controller { const salarys = salary_raw.length > 1 ? salary_raw.slice(1).map((item) => ({ - date: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiShortDate(item.commandDateAffect)) - : null, - position: Extension.ToThaiNumber( - Extension.ToThaiNumber( - `${item.positionName != null ? item.positionName : "-"} ${item.positionType == null ? item.positionCee ?? "" : (item.positionType == "อำนวยการ" || item.positionType == "บริหาร" ? item.positionType : "") + item.positionLevel}`, + date: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiShortDate(item.commandDateAffect)) + : null, + position: Extension.ToThaiNumber( + Extension.ToThaiNumber( + `${item.positionName != null ? item.positionName : "-"} ${item.positionType == null ? item.positionCee ?? "" : (item.positionType == "อำนวยการ" || item.positionType == "บริหาร" ? item.positionType : "") + item.positionLevel}`, + ), ), - ), - posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : "", - orgRoot: item.orgRoot != null ? Extension.ToThaiNumber(item.orgRoot) : "", - orgChild1: item.orgChild1 != null ? Extension.ToThaiNumber(item.orgChild1) : "", - orgChild2: item.orgChild2 != null ? Extension.ToThaiNumber(item.orgChild2) : "", - orgChild3: item.orgChild3 != null ? Extension.ToThaiNumber(item.orgChild3) : "", - orgChild4: item.orgChild4 != null ? Extension.ToThaiNumber(item.orgChild4) : "", - positionCee: item.positionCee != null ? Extension.ToThaiNumber(item.positionCee) : "", - positionExecutive: - item.positionExecutive != null ? Extension.ToThaiNumber(item.positionExecutive) : "", - })) + posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : "", + orgRoot: item.orgRoot != null ? Extension.ToThaiNumber(item.orgRoot) : "", + orgChild1: item.orgChild1 != null ? Extension.ToThaiNumber(item.orgChild1) : "", + orgChild2: item.orgChild2 != null ? Extension.ToThaiNumber(item.orgChild2) : "", + orgChild3: item.orgChild3 != null ? Extension.ToThaiNumber(item.orgChild3) : "", + orgChild4: item.orgChild4 != null ? Extension.ToThaiNumber(item.orgChild4) : "", + positionCee: item.positionCee != null ? Extension.ToThaiNumber(item.positionCee) : "", + positionExecutive: + item.positionExecutive != null ? Extension.ToThaiNumber(item.positionExecutive) : "", + })) : [ - { - date: "-", - position: "-", - posNo: "-", - orgRoot: null, - orgChild1: null, - orgChild2: null, - orgChild3: null, - orgChild4: null, - positionCee: null, - positionExecutive: null, - }, - ]; + { + date: "-", + position: "-", + posNo: "-", + orgRoot: null, + orgChild1: null, + orgChild2: null, + orgChild3: null, + orgChild4: null, + positionCee: null, + positionExecutive: null, + }, + ]; const educations = await this.profileEducationRepo.find({ select: [ @@ -342,20 +342,20 @@ export class ProfileController extends Controller { const Education = educations && educations.length > 0 ? educations.map((item) => ({ - institute: item.institute ? item.institute : "-", - date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "-", - degree: item.degree ? item.degree : "-", - })) + institute: item.institute ? item.institute : "-", + date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "-", + degree: item.degree ? item.degree : "-", + })) : [ - { - institute: "-", - date: "-", - degree: "-", - }, - ]; + { + institute: "-", + date: "-", + degree: "-", + }, + ]; const mapData = { // Id: profile.id, @@ -393,10 +393,10 @@ export class ProfileController extends Controller { position: salary_raw.length > 0 && salary_raw[0].positionName != null ? Extension.ToThaiNumber( - Extension.ToThaiNumber( - `${salary_raw[0].positionName != null ? salary_raw[0].positionName : "-"} ${salary_raw[0].positionType == null ? salary_raw[0].positionCee ?? "" : (salary_raw[0].positionType == "อำนวยการ" || salary_raw[0].positionType == "บริหาร" ? salary_raw[0].positionType : "") + salary_raw[0].positionLevel}`, - ), - ) + Extension.ToThaiNumber( + `${salary_raw[0].positionName != null ? salary_raw[0].positionName : "-"} ${salary_raw[0].positionType == null ? salary_raw[0].positionCee ?? "" : (salary_raw[0].positionType == "อำนวยการ" || salary_raw[0].positionType == "บริหาร" ? salary_raw[0].positionType : "") + salary_raw[0].positionLevel}`, + ), + ) : "", positionCee: salary_raw.length > 0 && salary_raw[0].positionCee != null @@ -406,22 +406,27 @@ export class ProfileController extends Controller { salary_raw.length > 0 && salary_raw[0].positionExecutive != null ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].positionExecutive)) : "", - org: `${salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-" - ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild4)) + " " - : "" - }${salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-" + org: `${ + salary_raw.length > 0 && salary_raw[0].orgChild4 && salary_raw[0].orgChild4 != "-" + ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild4)) + " " + : "" + }${ + salary_raw.length > 0 && salary_raw[0].orgChild3 && salary_raw[0].orgChild3 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild3)) + " " : "" - }${salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-" + }${ + salary_raw.length > 0 && salary_raw[0].orgChild2 && salary_raw[0].orgChild2 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild2)) + " " : "" - }${salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-" + }${ + salary_raw.length > 0 && salary_raw[0].orgChild1 && salary_raw[0].orgChild1 != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgChild1)) + " " : "" - }${salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-" + }${ + salary_raw.length > 0 && salary_raw[0].orgRoot && salary_raw[0].orgRoot != "-" ? Extension.ToThaiNumber(Extension.ToThaiNumber(salary_raw[0].orgRoot)) : "" - }`, + }`, ocFullPath: (_child4 == null ? "" : _child4 + "\n") + (_child3 == null ? "" : _child3 + "\n") + @@ -490,7 +495,7 @@ export class ProfileController extends Controller { }, }); _ImgUrl[i] = response_.data.downloadUrl; - } catch { } + } catch {} } }), ); @@ -504,7 +509,7 @@ export class ProfileController extends Controller { }, }); ImgUrl = response_.data.downloadUrl; - } catch { } + } catch {} } const profileOc = await this.profileRepo.findOne({ relations: [ @@ -542,36 +547,36 @@ export class ProfileController extends Controller { const root = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot; const child1 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1; const child2 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2; const child3 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3; const child4 = profileOc.current_holders == null || - profileOc.current_holders.length == 0 || - profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profileOc.current_holders.length == 0 || + profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profileOc.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4; @@ -590,19 +595,19 @@ export class ProfileController extends Controller { const certs = cert_raw.length > 0 ? cert_raw.slice(-2).map((item) => ({ - certificateType: item.certificateType ?? null, - issuer: item.issuer ?? null, - certificateNo: Extension.ToThaiNumber(item.certificateNo) ?? null, - issueDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) ?? null, - })) + certificateType: item.certificateType ?? null, + issuer: item.issuer ?? null, + certificateNo: Extension.ToThaiNumber(item.certificateNo) ?? null, + issueDate: Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) ?? null, + })) : [ - { - certificateType: "-", - issuer: "-", - certificateNo: "-", - issueDate: "-", - }, - ]; + { + certificateType: "-", + issuer: "-", + certificateNo: "-", + issueDate: "-", + }, + ]; const training_raw = await this.trainingRepository.find({ select: ["startDate", "endDate", "place", "department", "name", "isDeleted"], where: { profileId: id, isDeleted: false }, @@ -611,34 +616,34 @@ export class ProfileController extends Controller { const trainings = training_raw.length > 0 ? training_raw.slice(-2).map((item) => ({ - institute: item.department ?? "", - start: - item.startDate == null - ? "" - : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)), - end: - item.endDate == null - ? "" - : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)), - date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "", - level: "", - degree: item.name, - field: "", - })) + institute: item.department ?? "", + start: + item.startDate == null + ? "" + : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)), + end: + item.endDate == null + ? "" + : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)), + date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "", + level: "", + degree: item.name, + field: "", + })) : [ - { - institute: "-", - start: "-", - end: "-", - date: "-", - level: "-", - degree: "-", - field: "-", - }, - ]; + { + institute: "-", + start: "-", + end: "-", + date: "-", + level: "-", + degree: "-", + field: "-", + }, + ]; const discipline_raw = await this.disciplineRepository.find({ select: ["refCommandDate", "refCommandNo", "detail", "isDeleted"], @@ -648,19 +653,19 @@ export class ProfileController extends Controller { const disciplines = discipline_raw.length > 0 ? discipline_raw.slice(-2).map((item) => ({ - disciplineYear: - Extension.ToThaiNumber(new Date(item.refCommandDate).getFullYear().toString()) ?? - null, - disciplineDetail: item.detail ?? null, - refNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, - })) + disciplineYear: + Extension.ToThaiNumber(new Date(item.refCommandDate).getFullYear().toString()) ?? + null, + disciplineDetail: item.detail ?? null, + refNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, + })) : [ - { - disciplineYear: "-", - disciplineDetail: "-", - refNo: "-", - }, - ]; + { + disciplineYear: "-", + disciplineDetail: "-", + refNo: "-", + }, + ]; const education_raw = await this.profileEducationRepo.find({ select: [ @@ -679,34 +684,34 @@ export class ProfileController extends Controller { const educations = education_raw.length > 0 ? education_raw.slice(-2).map((item) => ({ - institute: item.institute, - start: - item.startDate == null - ? "" - : Extension.ToThaiNumber(new Date(item.startDate).getFullYear().toString()), - end: - item.endDate == null - ? "" - : Extension.ToThaiNumber(new Date(item.endDate).getFullYear().toString()), - date: - item.startDate && item.endDate - ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` - : "", - level: item.educationLevel ?? "", - degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", - field: item.field ?? "-", - })) + institute: item.institute, + start: + item.startDate == null + ? "" + : Extension.ToThaiNumber(new Date(item.startDate).getFullYear().toString()), + end: + item.endDate == null + ? "" + : Extension.ToThaiNumber(new Date(item.endDate).getFullYear().toString()), + date: + item.startDate && item.endDate + ? `${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate))} - ${Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate))}` + : "", + level: item.educationLevel ?? "", + degree: item.degree ? `${item.degree} ${item.field ? item.field : ""}` : "", + field: item.field ?? "-", + })) : [ - { - institute: "-", - start: "-", - end: "-", - date: "-", - level: "-", - degree: "-", - field: "-", - }, - ]; + { + institute: "-", + start: "-", + end: "-", + date: "-", + level: "-", + degree: "-", + field: "-", + }, + ]; const salary_raw = await this.salaryRepo.find({ select: [ "commandDateAffect", @@ -726,45 +731,45 @@ export class ProfileController extends Controller { const salarys = salary_raw.length > 0 ? salary_raw.map((item) => ({ - salaryDate: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : null, - position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, - posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : null, - salary: - item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, - rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, - refAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, - positionLevel: - item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, - positionType: item.positionType ?? null, - positionAmount: - item.positionSalaryAmount == null - ? null - : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), - fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, - ocFullPath: - (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root), - })) + salaryDate: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : null, + position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, + posNo: item.posNo != null ? Extension.ToThaiNumber(item.posNo) : null, + salary: + item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, + rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, + refAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, + positionLevel: + item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, + positionType: item.positionType ?? null, + positionAmount: + item.positionSalaryAmount == null + ? null + : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), + fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, + ocFullPath: + (_child4 == null ? "" : _child4 + "\n") + + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root), + })) : [ - { - salaryDate: "-", - position: "-", - posNo: "-", - salary: "-", - rank: "-", - refAll: "-", - positionLevel: "-", - positionType: "-", - positionAmount: "-", - fullName: "-", - ocFullPath: "-", - }, - ]; + { + salaryDate: "-", + position: "-", + posNo: "-", + salary: "-", + rank: "-", + refAll: "-", + positionLevel: "-", + positionType: "-", + positionAmount: "-", + fullName: "-", + ocFullPath: "-", + }, + ]; const insignia_raw = await this.profileInsigniaRepo.find({ relations: { @@ -778,37 +783,37 @@ export class ProfileController extends Controller { const insignias = insignia_raw.length > 0 ? insignia_raw.map((item) => ({ - receiveDate: item.receiveDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) - : "", - insigniaName: item.insignia.name, - insigniaShortName: item.insignia.shortName, - insigniaTypeName: item.insignia.insigniaType.name, - no: item.no ? Extension.ToThaiNumber(item.no) : "", - issue: item.issue ? item.issue : "", - volumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", - volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", - section: item.section ? Extension.ToThaiNumber(item.section) : "", - page: item.page ? Extension.ToThaiNumber(item.page) : "", - refCommandDate: item.refCommandDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) - : "", - })) + receiveDate: item.receiveDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) + : "", + insigniaName: item.insignia.name, + insigniaShortName: item.insignia.shortName, + insigniaTypeName: item.insignia.insigniaType.name, + no: item.no ? Extension.ToThaiNumber(item.no) : "", + issue: item.issue ? item.issue : "", + volumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", + volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", + section: item.section ? Extension.ToThaiNumber(item.section) : "", + page: item.page ? Extension.ToThaiNumber(item.page) : "", + refCommandDate: item.refCommandDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.refCommandDate)) + : "", + })) : [ - { - receiveDate: "-", - insigniaName: "-", - insigniaShortName: "-", - insigniaTypeName: "-", - no: "-", - issue: "-", - volumeNo: "-", - volume: "-", - section: "-", - page: "-", - refCommandDate: "-", - }, - ]; + { + receiveDate: "-", + insigniaName: "-", + insigniaShortName: "-", + insigniaTypeName: "-", + no: "-", + issue: "-", + volumeNo: "-", + volume: "-", + section: "-", + page: "-", + refCommandDate: "-", + }, + ]; const leave_raw = await this.profileLeaveRepository.find({ relations: { leaveType: true }, @@ -818,19 +823,19 @@ export class ProfileController extends Controller { const leaves = leave_raw.length > 0 ? leave_raw.map((item) => ({ - leaveTypeName: item.leaveType.name, - dateLeaveStart: item.dateLeaveStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) - : "", - leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "", - })) + leaveTypeName: item.leaveType.name, + dateLeaveStart: item.dateLeaveStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + : "", + leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "", + })) : [ - { - leaveTypeName: "-", - dateLeaveStart: "-", - leaveDays: "-", - }, - ]; + { + leaveTypeName: "-", + dateLeaveStart: "-", + leaveDays: "-", + }, + ]; const data = { fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, @@ -857,20 +862,20 @@ export class ProfileController extends Controller { profiles.citizenId != null ? Extension.ToThaiNumber(profiles.citizenId.toString()) : "", fatherFullName: profileFamilyFather?.fatherPrefix || - profileFamilyFather?.fatherFirstName || - profileFamilyFather?.fatherLastName + profileFamilyFather?.fatherFirstName || + profileFamilyFather?.fatherLastName ? `${profileFamilyFather?.fatherPrefix ?? ""}${profileFamilyFather?.fatherFirstName ?? ""} ${profileFamilyFather?.fatherLastName ?? ""}`.trim() : null, motherFullName: profileFamilyMother?.motherPrefix || - profileFamilyMother?.motherFirstName || - profileFamilyMother?.motherLastName + profileFamilyMother?.motherFirstName || + profileFamilyMother?.motherLastName ? `${profileFamilyMother?.motherPrefix ?? ""}${profileFamilyMother?.motherFirstName ?? ""} ${profileFamilyMother?.motherLastName ?? ""}`.trim() : null, coupleFullName: profileFamilyCouple?.couplePrefix || - profileFamilyCouple?.coupleFirstName || - profileFamilyCouple?.coupleLastNameOld + profileFamilyCouple?.coupleFirstName || + profileFamilyCouple?.coupleLastNameOld ? `${profileFamilyCouple?.couplePrefix ?? ""}${profileFamilyCouple?.coupleFirstName ?? ""} ${profileFamilyCouple?.coupleLastName ?? ""}`.trim() : null, coupleLastNameOld: profileFamilyCouple?.coupleLastNameOld ?? null, @@ -987,7 +992,7 @@ export class ProfileController extends Controller { }, }); _ImgUrl[i] = response_.data.downloadUrl; - } catch { } + } catch {} } }), ); @@ -1001,7 +1006,7 @@ export class ProfileController extends Controller { }, }); ImgUrl = response_.data.downloadUrl; - } catch { } + } catch {} } const orgRevision = await this.orgRevisionRepo.findOne({ @@ -1028,43 +1033,43 @@ export class ProfileController extends Controller { const posMasterId = profiles.current_holders == null || - profiles.current_holders.length == 0 || - profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.id; const root = profiles.current_holders == null || - profiles.current_holders.length == 0 || - profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot; const child1 = profiles.current_holders == null || - profiles.current_holders.length == 0 || - profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1; const child2 = profiles.current_holders == null || - profiles.current_holders.length == 0 || - profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2; const child3 = profiles.current_holders == null || - profiles.current_holders.length == 0 || - profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3; const child4 = profiles.current_holders == null || - profiles.current_holders.length == 0 || - profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null + profiles.current_holders.length == 0 || + profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) == null ? null : profiles.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4; @@ -1090,31 +1095,31 @@ export class ProfileController extends Controller { const certs = cert_raw.length > 0 ? cert_raw.map((item) => ({ - certificateType: item.certificateType ?? null, - issuer: item.issuer ?? null, - certificateNo: item.certificateNo ? Extension.ToThaiNumber(item.certificateNo) : null, - detail: Extension.ToThaiNumber( - `${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim(), - ), - issueToExpireDate: item.issueDate - ? item.expireDate - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.issueDate)} - ${Extension.ToThaiFullDate2(item.expireDate)}`, - ) - : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) - : item.expireDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) - : "", - })) + certificateType: item.certificateType ?? null, + issuer: item.issuer ?? null, + certificateNo: item.certificateNo ? Extension.ToThaiNumber(item.certificateNo) : null, + detail: Extension.ToThaiNumber( + `${item.issuer ?? ""} ${item.certificateNo ?? ""}`.trim(), + ), + issueToExpireDate: item.issueDate + ? item.expireDate + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.issueDate)} - ${Extension.ToThaiFullDate2(item.expireDate)}`, + ) + : Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.issueDate)) + : item.expireDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.expireDate)) + : "", + })) : [ - { - certificateType: "", - issuer: "", - certificateNo: "", - detail: "", - issueToExpireDate: "", - }, - ]; + { + certificateType: "", + issuer: "", + certificateNo: "", + detail: "", + issueToExpireDate: "", + }, + ]; const training_raw = await this.trainingRepository.find({ select: ["place", "department", "name", "duration", "isDeleted", "startDate", "endDate"], where: { profileId: id, isDeleted: false }, @@ -1123,23 +1128,23 @@ export class ProfileController extends Controller { const trainings = training_raw.length > 0 ? training_raw.map((item) => ({ - institute: item.department ?? "", - degree: item.name ? Extension.ToThaiNumber(item.name) : "", - place: item.place ? Extension.ToThaiNumber(item.place) : "", - duration: item.duration ? Extension.ToThaiNumber(item.duration) : "", - date: Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`, - ), - })) + institute: item.department ?? "", + degree: item.name ? Extension.ToThaiNumber(item.name) : "", + place: item.place ? Extension.ToThaiNumber(item.place) : "", + duration: item.duration ? Extension.ToThaiNumber(item.duration) : "", + date: Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.startDate)} - ${Extension.ToThaiFullDate2(item.endDate)}`, + ), + })) : [ - { - institute: "", - degree: "", - place: "", - duration: "", - date: "", - }, - ]; + { + institute: "", + degree: "", + place: "", + duration: "", + date: "", + }, + ]; const discipline_raw = await this.disciplineRepository.find({ select: ["refCommandDate", "refCommandNo", "detail", "level", "isDeleted"], @@ -1149,21 +1154,21 @@ export class ProfileController extends Controller { const disciplines = discipline_raw.length > 0 ? discipline_raw.map((item) => ({ - disciplineYear: item.refCommandDate - ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.refCommandDate))) - : null, - disciplineDetail: item.detail ?? null, - refNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, - level: item.level ?? "", - })) + disciplineYear: item.refCommandDate + ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.refCommandDate))) + : null, + disciplineDetail: item.detail ?? null, + refNo: Extension.ToThaiNumber(item.refCommandNo) ?? null, + level: item.level ?? "", + })) : [ - { - disciplineYear: "", - disciplineDetail: "", - refNo: "", - level: "", - }, - ]; + { + disciplineYear: "", + disciplineDetail: "", + refNo: "", + level: "", + }, + ]; const education_raw = await this.profileEducationRepo .createQueryBuilder("education") @@ -1175,21 +1180,21 @@ export class ProfileController extends Controller { const educations = education_raw.length > 0 ? education_raw.map((item) => ({ - institute: item.institute, - date: item.isDate - ? `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)) : ""}` - : `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.startDate))) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.endDate))) : ""}`, - degree: `${item.degree ?? ""} ${item.field ?? ""}`.trim(), - level: item.educationLevel, - })) + institute: item.institute, + date: item.isDate + ? `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.startDate)) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.endDate)) : ""}` + : `${item.startDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.startDate))) : ""} - ${item.endDate ? Extension.ToThaiNumber(Extension.ToThaiShortYear(new Date(item.endDate))) : ""}`, + degree: `${item.degree ?? ""} ${item.field ?? ""}`.trim(), + level: item.educationLevel, + })) : [ - { - institute: "", - date: "", - degree: "", - level: "", - }, - ]; + { + institute: "", + date: "", + degree: "", + level: "", + }, + ]; const salary_raw = await this.salaryRepo.find({ select: [ "commandName", @@ -1217,58 +1222,58 @@ export class ProfileController extends Controller { const salarys = salary_raw.length > 0 ? salary_raw.map((item) => ({ - commandName: item.commandName ?? "", - salaryDate: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : null, - position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, - posNo: - item.posNoAbb && item.posNo - ? Extension.ToThaiNumber(`${item.posNoAbb}${item.posNo}`) + commandName: item.commandName ?? "", + salaryDate: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) : null, - salary: - item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, - special: - item.amountSpecial != null - ? Extension.ToThaiNumber(item.amountSpecial.toLocaleString()) - : null, - rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, - refAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, - positionLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - positionType: item.positionType ?? null, - positionAmount: - item.positionSalaryAmount == null - ? null - : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), - fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, - ocFullPath: - (_child4 == null ? "" : _child4 + "\n") + - (_child3 == null ? "" : _child3 + "\n") + - (_child2 == null ? "" : _child2 + "\n") + - (_child1 == null ? "" : _child1 + "\n") + - (_root == null ? "" : _root), - })) + position: item.positionName != null ? Extension.ToThaiNumber(item.positionName) : null, + posNo: + item.posNoAbb && item.posNo + ? Extension.ToThaiNumber(`${item.posNoAbb}${item.posNo}`) + : null, + salary: + item.amount != null ? Extension.ToThaiNumber(item.amount.toLocaleString()) : null, + special: + item.amountSpecial != null + ? Extension.ToThaiNumber(item.amountSpecial.toLocaleString()) + : null, + rank: item.positionLevel != null ? Extension.ToThaiNumber(item.positionLevel) : null, + refAll: item.remark ? Extension.ToThaiNumber(item.remark) : null, + positionLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + positionType: item.positionType ?? null, + positionAmount: + item.positionSalaryAmount == null + ? null + : Extension.ToThaiNumber(item.positionSalaryAmount.toLocaleString()), + fullName: `${profiles?.prefix}${profiles?.firstName} ${profiles?.lastName}`, + ocFullPath: + (_child4 == null ? "" : _child4 + "\n") + + (_child3 == null ? "" : _child3 + "\n") + + (_child2 == null ? "" : _child2 + "\n") + + (_child1 == null ? "" : _child1 + "\n") + + (_root == null ? "" : _root), + })) : [ - { - commandName: "", - salaryDate: "", - position: "", - posNo: "", - salary: "", - special: "", - rank: "", - refAll: "", - positionLevel: "", - positionType: "", - positionAmount: "", - fullName: "", - ocFullPath: "", - }, - ]; + { + commandName: "", + salaryDate: "", + position: "", + posNo: "", + salary: "", + special: "", + rank: "", + refAll: "", + positionLevel: "", + positionType: "", + positionAmount: "", + fullName: "", + ocFullPath: "", + }, + ]; const insignia_raw = await this.profileInsigniaRepo.find({ select: [ @@ -1294,41 +1299,41 @@ export class ProfileController extends Controller { const insignias = insignia_raw.length > 0 ? insignia_raw.map((item) => ({ - receiveDate: item.receiveDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) - : "", - insigniaName: item.insignia?.name ?? "", - insigniaShortName: item.insignia?.shortName ?? "", - insigniaTypeName: item.insignia?.insigniaType?.name ?? "", - no: item.no ? Extension.ToThaiNumber(item.no) : "", - issue: item.issue ? Extension.ToThaiNumber(item.issue) : "", - volumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", - volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", - section: item.section ? Extension.ToThaiNumber(item.section) : "", - page: item.page ? Extension.ToThaiNumber(item.page) : "", - refCommandDate: item.refCommandDate - ? Extension.ToThaiNumber( - `ราชกิจจานุเบกษา เล่มที่ ${item.volume ?? "-"} ตอนที่ ${item.section ?? "-"} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`, - ) - : "-", - note: item.note ? Extension.ToThaiNumber(item.note) : "", - })) + receiveDate: item.receiveDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.receiveDate)) + : "", + insigniaName: item.insignia?.name ?? "", + insigniaShortName: item.insignia?.shortName ?? "", + insigniaTypeName: item.insignia?.insigniaType?.name ?? "", + no: item.no ? Extension.ToThaiNumber(item.no) : "", + issue: item.issue ? Extension.ToThaiNumber(item.issue) : "", + volumeNo: item.volumeNo ? Extension.ToThaiNumber(item.volumeNo) : "", + volume: item.volume ? Extension.ToThaiNumber(item.volume) : "", + section: item.section ? Extension.ToThaiNumber(item.section) : "", + page: item.page ? Extension.ToThaiNumber(item.page) : "", + refCommandDate: item.refCommandDate + ? Extension.ToThaiNumber( + `ราชกิจจานุเบกษา เล่มที่ ${item.volume ?? "-"} ตอนที่ ${item.section ?? "-"} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`, + ) + : "-", + note: item.note ? Extension.ToThaiNumber(item.note) : "", + })) : [ - { - receiveDate: "", - insigniaName: "", - insigniaShortName: "", - insigniaTypeName: "", - no: "", - issue: "", - volumeNo: "", - volume: "", - section: "", - page: "", - refCommandDate: "", - note: "", - }, - ]; + { + receiveDate: "", + insigniaName: "", + insigniaShortName: "", + insigniaTypeName: "", + no: "", + issue: "", + volumeNo: "", + volume: "", + section: "", + page: "", + refCommandDate: "", + note: "", + }, + ]; const leave_raw = await this.profileLeaveRepository .createQueryBuilder("profileLeave") @@ -1466,62 +1471,62 @@ export class ProfileController extends Controller { const leaves2 = leave2_raw.length > 0 ? leave2_raw.map((item) => { - const leaveTypeCode = item.code ? item.code.trim().toUpperCase() : ""; + const leaveTypeCode = item.code ? item.code.trim().toUpperCase() : ""; - // ข้อที่ 1: LV-008 ให้ใช้ leaveSubTypeName (ประเภทย่อย) แทน name - const displayType = - leaveTypeCode === "LV-008" && item.leaveSubTypeName - ? item.leaveSubTypeName - : item.name || "-"; + // ข้อที่ 1: LV-008 ให้ใช้ leaveSubTypeName (ประเภทย่อย) แทน name + const displayType = + leaveTypeCode === "LV-008" && item.leaveSubTypeName + ? item.leaveSubTypeName + : item.name || "-"; - // ข้อที่ 2: แสดง reason ก่อนเสมอ ถ้ามี coupleDayLevelCountry ค่อยแสดงประเทศ - const displayReason = item.coupleDayLevelCountry - ? `${item.reason || ""} ลาไปประเทศ ${item.coupleDayLevelCountry}`.trim() - : item.reason || "-"; + // ข้อที่ 2: แสดง reason ก่อนเสมอ ถ้ามี coupleDayLevelCountry ค่อยแสดงประเทศ + const displayReason = item.coupleDayLevelCountry + ? `${item.reason || ""} ลาไปประเทศ ${item.coupleDayLevelCountry}`.trim() + : item.reason || "-"; - return { - date: - item.dateLeaveStart && item.dateLeaveEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + - " - " + - Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveEnd)) - : "-", - type: displayType, - leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "-", - reason: displayReason, - }; - }) + return { + date: + item.dateLeaveStart && item.dateLeaveEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveStart)) + + " - " + + Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateLeaveEnd)) + : "-", + type: displayType, + leaveDays: item.leaveDays ? Extension.ToThaiNumber(item.leaveDays.toString()) : "-", + reason: displayReason, + }; + }) : [ - { - date: "", - type: "", - leaveDays: "", - reason: "", - }, - ]; + { + date: "", + type: "", + leaveDays: "", + reason: "", + }, + ]; const children_raw = await this.profileChildrenRepository.find({ where: { profileId: id, isDeleted: false }, }); const children = children_raw.length > 0 ? children_raw.map((item, index) => ({ - no: Extension.ToThaiNumber((index + 1).toString()), - childrenPrefix: item.childrenPrefix, - childrenFirstName: item.childrenFirstName, - childrenLastName: item.childrenLastName, - childrenFullName: `${item.childrenPrefix}${item.childrenFirstName} ${item.childrenLastName}`, - childrenLive: item.childrenLive == false ? "ถึงแก่กรรม" : "มีชีวิต", - })) + no: Extension.ToThaiNumber((index + 1).toString()), + childrenPrefix: item.childrenPrefix, + childrenFirstName: item.childrenFirstName, + childrenLastName: item.childrenLastName, + childrenFullName: `${item.childrenPrefix}${item.childrenFirstName} ${item.childrenLastName}`, + childrenLive: item.childrenLive == false ? "ถึงแก่กรรม" : "มีชีวิต", + })) : [ - { - no: "", - childrenPrefix: "", - childrenFirstName: "", - childrenLastName: "", - childrenFullName: "", - childrenLive: "", - }, - ]; + { + no: "", + childrenPrefix: "", + childrenFirstName: "", + childrenLastName: "", + childrenFullName: "", + childrenLive: "", + }, + ]; const changeName_raw = await this.changeNameRepository.find({ where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, @@ -1529,23 +1534,23 @@ export class ProfileController extends Controller { const changeName = changeName_raw.length > 0 ? changeName_raw.map((item) => ({ - createdAt: item.createdAt - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.createdAt)) - : null, - status: item.status, - prefix: item.prefix, - firstName: item.firstName, - lastName: item.lastName, - })) + createdAt: item.createdAt + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.createdAt)) + : null, + status: item.status, + prefix: item.prefix, + firstName: item.firstName, + lastName: item.lastName, + })) : [ - { - createdAt: "", - status: "", - prefix: "", - firstName: "", - lastName: "", - }, - ]; + { + createdAt: "", + status: "", + prefix: "", + firstName: "", + lastName: "", + }, + ]; const profileHistory = await this.profileHistoryRepo.find({ where: { profileId: id }, @@ -1554,19 +1559,19 @@ export class ProfileController extends Controller { const history = profileHistory.length > 0 ? profileHistory.map((item) => ({ - birthDateOld: item.birthDateOld - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.birthDateOld)) - : "", - birthDate: item.birthDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.birthDate)) - : "", - })) + birthDateOld: item.birthDateOld + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.birthDateOld)) + : "", + birthDate: item.birthDate + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.birthDate)) + : "", + })) : [ - { - birthDateOld: "", - birthDate: "", - }, - ]; + { + birthDateOld: "", + birthDate: "", + }, + ]; const position_raw = await this.salaryRepo.find({ where: [ @@ -1600,158 +1605,109 @@ export class ProfileController extends Controller { const positionList = position_raw.length > 0 ? await Promise.all( - position_raw.map(async (item, idx, arr) => { - const isLast = idx === arr.length - 1; - if (isLast) { - _commandDateAffect = item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : ""; - } - const _code = item.commandCode ? Number(item.commandCode) : null; - if (_code != null) { - _commandName = await this.commandCodeRepository.findOne({ - select: { name: true, code: true }, - where: { code: _code }, - }); - } - const codeSitAbb = item.posNumCodeSitAbb ?? "-"; - const commandNo = - item.commandNo && item.commandYear - ? item.commandNo + - "/" + - (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + position_raw.map(async (item, idx, arr) => { + const isLast = idx === arr.length - 1; + if (isLast) { + _commandDateAffect = item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : ""; + } + const _code = item.commandCode ? Number(item.commandCode) : null; + if (_code != null) { + _commandName = await this.commandCodeRepository.findOne({ + select: { name: true, code: true }, + where: { code: _code }, + }); + } + const codeSitAbb = item.posNumCodeSitAbb ?? "-"; + const commandNo = + item.commandNo && item.commandYear + ? item.commandNo + + "/" + + (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-"; + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` : "-"; - const dateAffect = item.commandDateAffect - ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` - : "-"; - return { - commandName: - _commandName && _commandName.name ? _commandName.name : item.commandName, - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - posNo: - item.posNoAbb && item.posNo - ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + return { + commandName: + _commandName && _commandName.name ? _commandName.name : item.commandName, + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) : "", - position: item.positionName, - posType: item.positionType, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount - ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) - : "", - positionSalaryAmount: item.positionSalaryAmount - ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) - : "", - refDoc: Extension.ToThaiNumber( - `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, - ), - }; - }), - ) + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + posNo: + item.posNoAbb && item.posNo + ? Extension.ToThaiNumber(`${item.posNoAbb} ${item.posNo}`) + : "", + position: item.positionName, + posType: item.positionType, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount + ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) + : "", + positionSalaryAmount: item.positionSalaryAmount + ? Extension.ToThaiNumber(Number(item.positionSalaryAmount).toLocaleString()) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, + ), + }; + }), + ) : [ - { - commandName: "", - commandDateAffect: "", - commandDateSign: "", - posNo: "", - position: "", - posType: "", - posLevel: "", - amount: "", - positionSalaryAmount: "", - refDoc: "", - }, - ]; + { + commandName: "", + commandDateAffect: "", + commandDateSign: "", + posNo: "", + position: "", + posType: "", + posLevel: "", + amount: "", + positionSalaryAmount: "", + refDoc: "", + }, + ]; // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); + // todo: รอข้อสรุป + // const retire_raw = await this.salaryRepo.findOne({ + // where: { + // profileId: id, + // commandCode: In(["12", "15", "16"]), + // }, + // order: { order: "desc" }, + // }); - // commandCode ที่ถือว่าออกจากราชการ - const retireCommandCodes = ["12", "15", "16"]; + // if (retire_raw) { + // const startDate = retire_raw.commandDateAffect; - // ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ - const salaries = await this.salaryRepo.find({ - where: { profileId: id }, - order: { order: "ASC" }, - }); + // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน + // let daysCount = 0; + // if (startDate) { + // const start = new Date(startDate); + // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + // } - // มีคำสั่งพ้นราชการหรือไม่ - if ( - salaries.length > 0 && - salaries.some((s) => s.commandCode && retireCommandCodes.includes(s.commandCode)) - ) { - // กรองข้อมูลซ้ำตาม commandDateAffect - const uniqueSalaries = salaries.filter( - (item, index, self) => - index === - self.findIndex( - (t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime(), - ), - ); + // const startDateStr = startDate + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + // : "-"; - // วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ" - for (let i = 0; i < uniqueSalaries.length; i++) { - const current = uniqueSalaries[i]; - - // เป็นคำสั่งออกจากราชการหรือไม่ - if (current.commandCode && retireCommandCodes.includes(current.commandCode)) { - const startDate = current.commandDateAffect; - let endDate: Date | null = null; - let endRecord = null; - - // หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ) - for (let j = i + 1; j < uniqueSalaries.length; j++) { - const next = uniqueSalaries[j]; - if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) { - endDate = next.commandDateAffect; - endRecord = next; - break; - } - } - - // ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน - if (!endDate) { - endDate = currentDate; - } - - // คำนวณจำนวนวัน - let daysCount = 0; - if (startDate && endDate) { - const start = new Date(startDate); - const end = new Date(endDate); - daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - } - - // สร้าง detail จาก commandName + remark - const commandName = current.commandName || ""; - const remark = current.remark || ""; - const detail = `${commandName} ${remark}`.trim(); - - // แปลงวันที่เป็น format ไทย - const startDateStr = startDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - : "-"; - const endDateStr = endDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) - : "-"; - - retires.push({ - date: `${startDateStr} - ${endDateStr}`, - detail: detail || "-", - day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-", - }); - } - } - } + // retires.push({ + // date: `${startDateStr}`, + // detail: retire_raw.commandName ?? "-", + // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + // }); + // } // กรณีไม่มีข้อมูล if (retires.length === 0) { @@ -1812,8 +1768,8 @@ export class ProfileController extends Controller { date: item.dateStart && item.dateEnd ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1865,8 +1821,8 @@ export class ProfileController extends Controller { date: item.dateStart && item.dateEnd ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) : item.dateStart ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) : item.dateEnd @@ -1890,36 +1846,36 @@ export class ProfileController extends Controller { const duty = duty_raw.length > 0 ? duty_raw.map((item) => ({ - date: - item.dateStart && item.dateEnd - ? Extension.ToThaiNumber( - `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, - ) - : item.dateStart - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) - : item.dateEnd - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) - : "", - type: "-", - detail: Extension.ToThaiNumber(item.detail), - agency: "-", - refCommandNo: item.refCommandNo - ? item.refCommandDate - ? Extension.ToThaiNumber( - `${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`, - ) - : Extension.ToThaiNumber(item.refCommandNo) - : "-", - })) + date: + item.dateStart && item.dateEnd + ? Extension.ToThaiNumber( + `${Extension.ToThaiFullDate2(item.dateStart)} - ${Extension.ToThaiFullDate2(item.dateEnd)}`, + ) + : item.dateStart + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateStart)) + : item.dateEnd + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.dateEnd)) + : "", + type: "-", + detail: Extension.ToThaiNumber(item.detail), + agency: "-", + refCommandNo: item.refCommandNo + ? item.refCommandDate + ? Extension.ToThaiNumber( + `${item.refCommandNo} ลว. ${Extension.ToThaiFullDate2(item.refCommandDate)}`, + ) + : Extension.ToThaiNumber(item.refCommandNo) + : "-", + })) : [ - { - date: "", - type: "", - detail: "", - agency: "", - refCommandNo: "", - }, - ]; + { + date: "", + type: "", + detail: "", + agency: "", + refCommandNo: "", + }, + ]; const assessments_raw = await this.profileAssessmentsRepository.find({ where: { profileId: id, isDeleted: false }, order: { createdAt: "ASC" }, @@ -1927,49 +1883,49 @@ export class ProfileController extends Controller { const assessments = assessments_raw.length > 0 ? assessments_raw.map((item) => ({ - year: item.year ? Extension.ToThaiNumber((parseInt(item.year) + 543).toString()) : "", - period: - item.period && item.period == "APR" - ? Extension.ToThaiNumber( - `1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`, - ) - : Extension.ToThaiNumber( - `1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`, - ), - point1: item.point1 ? Extension.ToThaiNumber(item.point1.toString()) : "", - point1Total: item.point1Total - ? Extension.ToThaiNumber(item.point1Total.toString()) - : "", - point2: item.point2 ? Extension.ToThaiNumber(item.point2.toString()) : "", - point2Total: item.point2Total - ? Extension.ToThaiNumber(item.point2Total.toString()) - : "", - pointSum: item.pointSum - ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) - : "", - pointSumTh: item.pointSum ? Extension.textPoint(item.pointSum) : "", - level: - item.pointSum < 60.0 - ? "ต้องปรับปรุง" - : item.pointSum <= 69.99 && item.pointSum >= 60.0 - ? "พอใช้" - : item.pointSum <= 79.99 && item.pointSum >= 70.0 - ? "ดี" - : item.pointSum <= 89.99 && item.pointSum >= 80.0 - ? "ดีมาก" - : "ดีเด่น", - })) + year: item.year ? Extension.ToThaiNumber((parseInt(item.year) + 543).toString()) : "", + period: + item.period && item.period == "APR" + ? Extension.ToThaiNumber( + `1 เม.ย. ${(parseInt(item.year) + 543 - 1).toString()} - 31 มี.ค. ${(parseInt(item.year) + 543).toString()}`, + ) + : Extension.ToThaiNumber( + `1 ต.ค. ${(parseInt(item.year) + 543 - 1).toString()} - 30 ก.ย. ${(parseInt(item.year) + 543).toString()}`, + ), + point1: item.point1 ? Extension.ToThaiNumber(item.point1.toString()) : "", + point1Total: item.point1Total + ? Extension.ToThaiNumber(item.point1Total.toString()) + : "", + point2: item.point2 ? Extension.ToThaiNumber(item.point2.toString()) : "", + point2Total: item.point2Total + ? Extension.ToThaiNumber(item.point2Total.toString()) + : "", + pointSum: item.pointSum + ? Extension.ToThaiNumber(`ร้อยละ ${item.pointSum.toString()}`) + : "", + pointSumTh: item.pointSum ? Extension.textPoint(item.pointSum) : "", + level: + item.pointSum < 60.0 + ? "ต้องปรับปรุง" + : item.pointSum <= 69.99 && item.pointSum >= 60.0 + ? "พอใช้" + : item.pointSum <= 79.99 && item.pointSum >= 70.0 + ? "ดี" + : item.pointSum <= 89.99 && item.pointSum >= 80.0 + ? "ดีมาก" + : "ดีเด่น", + })) : [ - { - year: "", - period: "", - point1: "", - point2: "", - pointSum: "", - pointSumTh: "", - level: "", - }, - ]; + { + year: "", + period: "", + point1: "", + point2: "", + pointSum: "", + pointSumTh: "", + level: "", + }, + ]; const profileAbility_raw = await this.profileAbilityRepo.find({ where: { profileId: id }, order: { createdAt: "ASC" }, @@ -1977,15 +1933,15 @@ export class ProfileController extends Controller { const profileAbility = profileAbility_raw.length > 0 ? profileAbility_raw.map((item) => ({ - field: item.field ? item.field : "", - detail: item.detail ? item.detail : "", - })) + field: item.field ? item.field : "", + detail: item.detail ? item.detail : "", + })) : [ - { - field: "", - detail: "", - }, - ]; + { + field: "", + detail: "", + }, + ]; const otherIncome_raw = await this.salaryRepo.find({ where: { @@ -1998,95 +1954,95 @@ export class ProfileController extends Controller { const otherIncome = otherIncome_raw.length > 0 ? await Promise.all( - otherIncome_raw.map(async (item) => { - const codeSitAbb = item.posNumCodeSitAbb ?? "-"; - const commandNo = - item.commandNo && item.commandYear - ? item.commandNo + - "/" + - (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + otherIncome_raw.map(async (item) => { + const codeSitAbb = item.posNumCodeSitAbb ?? "-"; + const commandNo = + item.commandNo && item.commandYear + ? item.commandNo + + "/" + + (item.commandYear > 2500 ? item.commandYear : item.commandYear + 543) + : "-"; + const dateAffect = item.commandDateAffect + ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` : "-"; - const dateAffect = item.commandDateAffect - ? `${Extension.ToThaiFullDate2(item.commandDateAffect)}` - : "-"; - return { - commandName: item.commandName ?? "", - commandDateAffect: item.commandDateAffect - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) - : "", - commandDateSign: item.commandDateSign - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) - : "", - commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", - position: item.positionName, - posLevel: item.positionLevel - ? Extension.ToThaiNumber(item.positionLevel) - : item.positionCee - ? Extension.ToThaiNumber(item.positionCee) - : null, - amount: item.amount - ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) - : "", - refDoc: Extension.ToThaiNumber( - `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, - ), - }; - }), - ) + return { + commandName: item.commandName ?? "", + commandDateAffect: item.commandDateAffect + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateAffect)) + : "", + commandDateSign: item.commandDateSign + ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(item.commandDateSign)) + : "", + commandNo: item.commandNo ? Extension.ToThaiNumber(item.commandNo) : "", + position: item.positionName, + posLevel: item.positionLevel + ? Extension.ToThaiNumber(item.positionLevel) + : item.positionCee + ? Extension.ToThaiNumber(item.positionCee) + : null, + amount: item.amount + ? Extension.ToThaiNumber(Number(item.amount).toLocaleString()) + : "", + refDoc: Extension.ToThaiNumber( + `คำสั่ง ${codeSitAbb} ที่ ${commandNo} ลว. ${dateAffect}`, + ), + }; + }), + ) : [ - { - commandName: "", - commandDateAffect: "", - commandDateSign: "", - commandNo: "", - position: "", - posLevel: "", - amount: "", - refDoc: "", - }, - ]; + { + commandName: "", + commandDateAffect: "", + commandDateSign: "", + commandNo: "", + position: "", + posLevel: "", + amount: "", + refDoc: "", + }, + ]; const sum = profiles ? Extension.ToThaiNumber( - ( - Number(profiles.amount) + - Number(profiles.positionSalaryAmount) + - Number(profiles.mouthSalaryAmount) + - Number(profiles.amountSpecial) - ).toLocaleString(), - ) + ( + Number(profiles.amount) + + Number(profiles.positionSalaryAmount) + + Number(profiles.mouthSalaryAmount) + + Number(profiles.amountSpecial) + ).toLocaleString(), + ) : ""; const fullCurrentAddress = profiles && profiles.currentAddress ? Extension.ToThaiNumber( - profiles.currentAddress + - (profiles.currentSubDistrict && profiles.currentSubDistrict.name - ? " ตำบล/แขวง " + profiles.currentSubDistrict.name - : "") + - (profiles.currentDistrict && profiles.currentDistrict.name - ? " อำเภอ/เขต " + profiles.currentDistrict.name - : "") + - (profiles.currentProvince && profiles.currentProvince.name - ? " จังหวัด " + profiles.currentProvince.name - : "") + - (profiles.currentZipCode ? " " + profiles.currentZipCode : ""), - ) + profiles.currentAddress + + (profiles.currentSubDistrict && profiles.currentSubDistrict.name + ? " ตำบล/แขวง " + profiles.currentSubDistrict.name + : "") + + (profiles.currentDistrict && profiles.currentDistrict.name + ? " อำเภอ/เขต " + profiles.currentDistrict.name + : "") + + (profiles.currentProvince && profiles.currentProvince.name + ? " จังหวัด " + profiles.currentProvince.name + : "") + + (profiles.currentZipCode ? " " + profiles.currentZipCode : ""), + ) : ""; const fullRegistrationAddress = profiles && profiles.registrationAddress ? Extension.ToThaiNumber( - profiles.registrationAddress + - (profiles.registrationSubDistrict && profiles.registrationSubDistrict.name - ? " ตำบล/แขวง " + profiles.registrationSubDistrict.name - : "") + - (profiles.registrationDistrict && profiles.registrationDistrict.name - ? " อำเภอ/เขต " + profiles.registrationDistrict.name - : "") + - (profiles.registrationProvince && profiles.registrationProvince.name - ? " จังหวัด " + profiles.registrationProvince.name - : "") + - (profiles.currentZipCode ? " " + profiles.currentZipCode : ""), - ) + profiles.registrationAddress + + (profiles.registrationSubDistrict && profiles.registrationSubDistrict.name + ? " ตำบล/แขวง " + profiles.registrationSubDistrict.name + : "") + + (profiles.registrationDistrict && profiles.registrationDistrict.name + ? " อำเภอ/เขต " + profiles.registrationDistrict.name + : "") + + (profiles.registrationProvince && profiles.registrationProvince.name + ? " จังหวัด " + profiles.registrationProvince.name + : "") + + (profiles.currentZipCode ? " " + profiles.currentZipCode : ""), + ) : ""; let portfolios: any[] = []; @@ -2095,7 +2051,7 @@ export class ProfileController extends Controller { .then((x) => { portfolios = Array.isArray(x) ? x : []; }) - .catch(() => { }); + .catch(() => {}); if (portfolios.length == 0) { portfolios = [{ name: "", year: "", position: "" }]; } else { @@ -2173,24 +2129,24 @@ export class ProfileController extends Controller { profiles.citizenId != null ? Extension.ToThaiNumber(profiles.citizenId.toString()) : "", fatherFullName: profileFamilyFather?.fatherPrefix || - profileFamilyFather?.fatherFirstName || - profileFamilyFather?.fatherLastName + profileFamilyFather?.fatherFirstName || + profileFamilyFather?.fatherLastName ? `${profileFamilyFather?.fatherPrefix ?? ""}${profileFamilyFather?.fatherFirstName ?? ""} ${profileFamilyFather?.fatherLastName ?? ""}`.trim() : null, fatherLive: profileFamilyFather && profileFamilyFather?.fatherLive == false ? "ถึงแก่กรรม" : "มีชีวิต", motherFullName: profileFamilyMother?.motherPrefix || - profileFamilyMother?.motherFirstName || - profileFamilyMother?.motherLastName + profileFamilyMother?.motherFirstName || + profileFamilyMother?.motherLastName ? `${profileFamilyMother?.motherPrefix ?? ""}${profileFamilyMother?.motherFirstName ?? ""} ${profileFamilyMother?.motherLastName ?? ""}`.trim() : null, motherLive: profileFamilyMother && profileFamilyMother?.motherLive == false ? "ถึงแก่กรรม" : "มีชีวิต", coupleFullName: profileFamilyCouple?.couplePrefix || - profileFamilyCouple?.coupleFirstName || - profileFamilyCouple?.coupleLastNameOld + profileFamilyCouple?.coupleFirstName || + profileFamilyCouple?.coupleLastNameOld ? `${profileFamilyCouple?.couplePrefix ?? ""}${profileFamilyCouple?.coupleFirstName ?? ""} ${profileFamilyCouple?.coupleLastName ?? ""}`.trim() : null, coupleLastNameOld: profileFamilyCouple?.coupleLastNameOld ?? null, @@ -2645,7 +2601,7 @@ export class ProfileController extends Controller { ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `current_holders.orgChild1Id is null` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -3433,44 +3389,7 @@ export class ProfileController extends Controller { .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getManyAndCount(); - - // ดึง posMasterId ทั้งหมด (36 ตัวแรกของ key) เพื่อ query positionName - const posMasterIds = lists - .map((x) => x.key?.substring(0, 36)) - .filter((id) => id && id.length === 36); - const posMasterPositionMap = await getPosMasterPositions(posMasterIds); - - // ปรับ positionSign สำหรับกรณีรักษาการ - const processedLists = lists.map((x: any) => { - let newPositionSign = x.positionSign; - - // ตำแหน่งของคนที่เลือกไปรักษาการ - let childPosition = ""; - if (x.posType === "อำนวยการ" || x.posType === "บริหาร") { - childPosition = x.posExecutiveName || ""; - if (!childPosition) { - childPosition = `${x.position || ""}ระดับ${x.posLevel || ""}`.trim(); - } - } 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, - positionSign: newPositionSign, - }; - }); - - return new HttpSuccess({ data: processedLists, total }); + return new HttpSuccess({ data: lists, total }); } else { const [lists, total] = await AppDataSource.getRepository(viewDirector) .createQueryBuilder("viewDirector") @@ -4064,24 +3983,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -4200,24 +4119,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -4331,24 +4250,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -4462,24 +4381,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -4593,24 +4512,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -4723,24 +4642,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -4847,24 +4766,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -4970,24 +4889,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -5093,24 +5012,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; @@ -5217,24 +5136,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -5340,24 +5259,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}}` : null; @@ -6025,12 +5944,12 @@ export class ProfileController extends Controller { queryLike = "profile.position LIKE :keyword"; } else if (searchField == "posNo") { queryLike = ` - CASE - WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(" ", orgChild4.orgChild4ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(" ", orgChild3.orgChild3ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(" ", orgChild2.orgChild2ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(" ", orgChild1.orgChild1ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - ELSE CONCAT_WS(" ", orgRoot.orgRootShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) + CASE + WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo) + ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo) END LIKE :keyword `; } @@ -6122,75 +6041,75 @@ export class ProfileController extends Controller { record.map((_data) => { const posExecutive = _data.current_holders.length == 0 || - _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.positions.length == 0 || - _data.current_holders - .find((x) => x.orgRevisionId == revisionId) - ?.positions.find((x: any) => x.positionIsSelected == true) == null || - _data.current_holders - .find((x) => x.orgRevisionId == revisionId) - ?.positions.find((x: any) => x.positionIsSelected == true)?.posExecutive == null + _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.positions.length == 0 || + _data.current_holders + .find((x) => x.orgRevisionId == revisionId) + ?.positions.find((x: any) => x.positionIsSelected == true) == null || + _data.current_holders + .find((x) => x.orgRevisionId == revisionId) + ?.positions.find((x: any) => x.positionIsSelected == true)?.posExecutive == null ? null : _data.current_holders - .find((x) => x.orgRevisionId == revisionId) - ?.positions.find((x: any) => x.positionIsSelected == true)?.posExecutive - ?.posExecutiveName; + .find((x) => x.orgRevisionId == revisionId) + ?.positions.find((x: any) => x.positionIsSelected == true)?.posExecutive + ?.posExecutiveName; const shortName = _data.current_holders.length == 0 ? null : _data.current_holders.find((x) => x.orgRevisionId == revisionId) != null && - _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 != null + _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 != null ? `${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.posMasterNo}` : _data.current_holders.find((x) => x.orgRevisionId == revisionId) != null && - _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 != - null + _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 != + null ? `${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3.orgChild3ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.posMasterNo}` : _data.current_holders.find((x) => x.orgRevisionId == revisionId) != null && - _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 != - null + _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 != + null ? `${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2.orgChild2ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.posMasterNo}` : _data.current_holders.find((x) => x.orgRevisionId == revisionId) != null && - _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 != - null + _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 != + null ? `${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1.orgChild1ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.posMasterNo}` : _data.current_holders.find((x) => x.orgRevisionId == revisionId) != null && - _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot != - null + _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot != + null ? `${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot.orgRootShortName} ${_data.current_holders.find((x) => x.orgRevisionId == revisionId)?.posMasterNo}` : null; const root = _data.current_holders.length == 0 || - (_data.current_holders.find((x) => x.orgRevisionId == revisionId) != null && - _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null) + (_data.current_holders.find((x) => x.orgRevisionId == revisionId) != null && + _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null) ? null : _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot; const child1 = _data.current_holders == null || - _data.current_holders.length == 0 || - _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null + _data.current_holders.length == 0 || + _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null ? null : _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1; const child2 = _data.current_holders == null || - _data.current_holders.length == 0 || - _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null + _data.current_holders.length == 0 || + _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null ? null : _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2; const child3 = _data.current_holders == null || - _data.current_holders.length == 0 || - _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null + _data.current_holders.length == 0 || + _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null ? null : _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3; const child4 = _data.current_holders == null || - _data.current_holders.length == 0 || - _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null + _data.current_holders.length == 0 || + _data.current_holders.find((x) => x.orgRevisionId == revisionId) == null ? null : _data.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4; let _root = root?.orgRootName; @@ -6615,12 +6534,12 @@ export class ProfileController extends Controller { queryLike = "profile.position LIKE :keyword"; } else if (searchField == "posNo") { queryLike = ` - CASE - WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(" ", orgChild4.orgChild4ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(" ", orgChild3.orgChild3ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(" ", orgChild2.orgChild2ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(" ", orgChild1.orgChild1ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - ELSE CONCAT_WS(" ", orgRoot.orgRootShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) + CASE + WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo) + ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo) END LIKE :keyword `; } @@ -6669,8 +6588,6 @@ export class ProfileController extends Controller { "posType.posTypeName", "current_holders.orgRevisionId", "current_holders.posMasterNo", - "current_holders.posMasterNoPrefix", - "current_holders.posMasterNoSuffix", "orgRoot.id", "orgRoot.ancestorDNA", "orgRoot.orgRootName", @@ -6710,7 +6627,7 @@ export class ProfileController extends Controller { ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `current_holders.orgChild1Id is null` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -6803,19 +6720,18 @@ export class ProfileController extends Controller { .filter(Boolean) .join("\n"); - const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : ''; const shortName = !holder ? null : holder.orgChild4 != null - ? `${holder.orgChild4.orgChild4ShortName} ${numPart}` + ? `${holder.orgChild4.orgChild4ShortName} ${holder.posMasterNo}` : holder.orgChild3 != null - ? `${holder.orgChild3.orgChild3ShortName} ${numPart}` + ? `${holder.orgChild3.orgChild3ShortName} ${holder.posMasterNo}` : holder.orgChild2 != null - ? `${holder.orgChild2.orgChild2ShortName} ${numPart}` + ? `${holder.orgChild2.orgChild2ShortName} ${holder.posMasterNo}` : holder.orgChild1 != null - ? `${holder.orgChild1.orgChild1ShortName} ${numPart}` + ? `${holder.orgChild1.orgChild1ShortName} ${holder.posMasterNo}` : holder.orgRoot != null - ? `${holder.orgRoot.orgRootShortName} ${numPart}` + ? `${holder.orgRoot.orgRootShortName} ${holder.posMasterNo}` : null; return { @@ -7009,12 +6925,12 @@ export class ProfileController extends Controller { queryLike = "profile.position LIKE :keyword"; } else if (searchField == "posNo") { queryLike = ` - CASE - WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(" ", orgChild4.orgChild4ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(" ", orgChild3.orgChild3ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(" ", orgChild2.orgChild2ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(" ", orgChild1.orgChild1ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - ELSE CONCAT_WS(" ", orgRoot.orgRootShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) + CASE + WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo) + ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo) END LIKE :keyword `; } @@ -7067,8 +6983,6 @@ export class ProfileController extends Controller { "posType.posTypeName", "current_holders.orgRevisionId", "current_holders.posMasterNo", - "current_holders.posMasterNoPrefix", - "current_holders.posMasterNoSuffix", "orgRoot.id", "orgRoot.ancestorDNA", "orgRoot.orgRootName", @@ -7114,7 +7028,7 @@ export class ProfileController extends Controller { ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `current_holders.orgChild1Id is null` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -7191,20 +7105,18 @@ export class ProfileController extends Controller { .filter(Boolean) .join("\n"); - const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : ''; - const shortName = !holder ? null : holder.orgChild4 != null - ? `${holder.orgChild4.orgChild4ShortName} ${numPart}` + ? `${holder.orgChild4.orgChild4ShortName} ${holder.posMasterNo}` : holder.orgChild3 != null - ? `${holder.orgChild3.orgChild3ShortName} ${numPart}` + ? `${holder.orgChild3.orgChild3ShortName} ${holder.posMasterNo}` : holder.orgChild2 != null - ? `${holder.orgChild2.orgChild2ShortName} ${numPart}` + ? `${holder.orgChild2.orgChild2ShortName} ${holder.posMasterNo}` : holder.orgChild1 != null - ? `${holder.orgChild1.orgChild1ShortName} ${numPart}` + ? `${holder.orgChild1.orgChild1ShortName} ${holder.posMasterNo}` : holder.orgRoot != null - ? `${holder.orgRoot.orgRootShortName} ${numPart}` + ? `${holder.orgRoot.orgRootShortName} ${holder.posMasterNo}` : null; return { @@ -7354,8 +7266,8 @@ export class ProfileController extends Controller { .map((x) => x.next_holderId).length == 0 ? ["zxc"] : orgRevision.posMasters - .filter((x) => x.next_holderId != null) - .map((x) => x.next_holderId), + .filter((x) => x.next_holderId != null) + .map((x) => x.next_holderId), }); }), ) @@ -7651,8 +7563,8 @@ export class ProfileController extends Controller { .map((x) => x.current_holderId).length == 0 ? ["zxc"] : orgRevision.posMasters - .filter((x) => x.current_holderId != null) - .map((x) => x.current_holderId), + .filter((x) => x.current_holderId != null) + .map((x) => x.current_holderId), }); }), ) @@ -7830,45 +7742,45 @@ export class ProfileController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild1; + ?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild2; + ?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild3; + ?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgChild4; + ?.orgChild4; const position = await this.positionRepository.findOne({ relations: ["posExecutive"], @@ -8009,37 +7921,37 @@ export class ProfileController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -8277,36 +8189,36 @@ export class ProfileController extends Controller { } const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -8513,11 +8425,11 @@ export class ProfileController extends Controller { commanderRootName_ = commandProfile?.current_holders == null || - commandProfile?.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRoot == null + commandProfile?.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgRoot == null ? null : commandProfile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRoot; + ?.orgRoot; //find สังกัดผู้บังคับบัญชา const commanderPosMaster_ = await this.posMasterRepo.findOne({ where: { @@ -8561,11 +8473,11 @@ export class ProfileController extends Controller { commanderAboveRootName_ = commandAboveProfile?.current_holders == null || - commandAboveProfile?.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRoot == null + commandAboveProfile?.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgRoot == null ? null : commandAboveProfile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) - ?.orgRoot; + ?.orgRoot; //find สังกัดผู้บังคับบัญชาเหนือไป1ขั้น const commanderAbovePosMaster_ = await this.posMasterRepo.findOne({ where: { @@ -8758,36 +8670,36 @@ export class ProfileController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -8797,21 +8709,32 @@ export class ProfileController extends Controller { posMasterId: posMaster?.id, }, }); - const holder = profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); - const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : ''; const shortName = - holder == null + profile.current_holders.length == 0 ? null - : holder.orgChild4 != null - ? `${holder.orgChild4.orgChild4ShortName} ${numPart}` - : holder.orgChild3 != null - ? `${holder.orgChild3.orgChild3ShortName} ${numPart}` - : holder.orgChild2 != null - ? `${holder.orgChild2.orgChild2ShortName} ${numPart}` - : holder.orgChild1 != null - ? `${holder.orgChild1.orgChild1ShortName} ${numPart}` - : holder.orgRoot != null - ? `${holder.orgRoot.orgRootShortName} ${numPart}` + : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild4 != null + ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4.orgChild4ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` + : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild3 != null + ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3.orgChild3ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` + : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild2 != null + ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2.orgChild2ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` + : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgChild1 != null + ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1.orgChild1ShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` + : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) != + null && + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) + ?.orgRoot != null + ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot.orgRootShortName} ${profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.posMasterNo}` : null; // const posMasterActs = await this.posMasterActRepository.find({ // relations: [ @@ -9043,13 +8966,13 @@ export class ProfileController extends Controller { "current_holders.orgChild2", "current_holders.orgChild3", "current_holders.orgChild4", - // "profileSalary", + "profileSalary", "profileEducations", ], order: { - // profileSalary: { - // order: "DESC", - // }, + profileSalary: { + order: "DESC", + }, profileEducations: { level: "ASC", }, @@ -9071,36 +8994,36 @@ export class ProfileController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -9173,32 +9096,26 @@ export class ProfileController extends Controller { profile.avatar && profile.avatarName ? `${profile.avatar}/${profile.avatarName}` : null, }; - const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : ''; if (_profile.child4Id != null) { _profile.node = 4; _profile.nodeId = _profile.child4Id; _profile.nodeShortName = _profile.child4ShortName; - _profile.posNo = `${_profile.child4ShortName} ${_numPart}`; } else if (_profile.child3Id != null) { _profile.node = 3; _profile.nodeId = _profile.child3Id; _profile.nodeShortName = _profile.child3ShortName; - _profile.posNo = `${_profile.child3ShortName} ${_numPart}`; } else if (_profile.child2Id != null) { _profile.node = 2; _profile.nodeId = _profile.child2Id; _profile.nodeShortName = _profile.child2ShortName; - _profile.posNo = `${_profile.child2ShortName} ${_numPart}`; } else if (_profile.child1Id != null) { _profile.node = 1; _profile.nodeId = _profile.child1Id; _profile.nodeShortName = _profile.child1ShortName; - _profile.posNo = `${_profile.child1ShortName} ${_numPart}`; } else if (_profile.rootId != null) { _profile.node = 0; _profile.nodeId = _profile.rootId; _profile.nodeShortName = _profile.rootShortName; - _profile.posNo = `${_profile.rootShortName} ${_numPart}`; } return new HttpSuccess(_profile); } @@ -9246,36 +9163,36 @@ export class ProfileController extends Controller { const posMaster = profile.current_holders == null || - profile.current_holders.length == 0 || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null + profile.current_holders.length == 0 || + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id) == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const root = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgRoot; const child1 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild1; const child2 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild2; const child3 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild3; const child4 = profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == + profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; @@ -9466,7 +9383,7 @@ export class ProfileController extends Controller { ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `current_holders.orgChild1Id is null` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1 }, ) @@ -9532,32 +9449,32 @@ export class ProfileController extends Controller { 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 != + 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 != + 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 != 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 != 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 + 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 root = item.current_holders.length == 0 || - (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) + (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; @@ -9991,7 +9908,7 @@ export class ProfileController extends Controller { ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `current_holders.orgChild1Id is null` + `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -10083,19 +10000,7 @@ export class ProfileController extends Controller { } else if (body.sortBy === "posTypeName") { query = query.orderBy(`posType.posTypeName`, body.descending ? "DESC" : "ASC"); } else if (body.sortBy === "commandNo") { - // Use subquery to get the latest commandNo for each profile - const subquery = AppDataSource.getRepository(ProfileSalary) - .createQueryBuilder("ps") - .select("ps.commandNo", "commandNo") - .where("ps.profileId = profile.id") - .orderBy("ps.order", "DESC") - .addOrderBy("ps.commandNo", "DESC") - .limit(1); - - query = query - .addSelect(`(${subquery.getSql()})`, "latestCommandNo") - .orderBy("latestCommandNo", body.descending ? "DESC" : "ASC") - .addOrderBy("profile.id", "ASC"); // Secondary sort for consistency + query = query.orderBy(`profileSalary.commandNo`, body.descending ? "DESC" : "ASC"); } else if (body.sortBy === "orgRootName") { query = query.orderBy(`orgRoot.orgRootName`, body.descending ? "DESC" : "ASC"); } else { @@ -10124,61 +10029,61 @@ export class ProfileController extends Controller { findProfile.map(async (item: Profile) => { const posMaster = item.current_holders == null || - item.current_holders.length == 0 || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null + item.current_holders.length == 0 || + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id); const position = posMaster == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.positions == null || - item.current_holders?.find((x) => x.orgRevisionId == findRevision.id)?.positions.length == + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.positions == null || + item.current_holders?.find((x) => x.orgRevisionId == findRevision.id)?.positions.length == 0 || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true) == null + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true) == null ? null : item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true); + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true); const posExecutive = position == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - ?.posExecutiveName == null + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive + ?.posExecutiveName == null ? null : item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - .posExecutiveName; + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive + .posExecutiveName; 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 != + 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 != + 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 != 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 != 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 + 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; @@ -10204,154 +10109,154 @@ export class ProfileController extends Controller { isProbation: item.isProbation, orgRootName: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot + ?.orgRootName == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName, + ?.orgRootName, orgChild1Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 - ?.orgChild1Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 + ?.orgChild1Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild1?.orgChild1Name, + ?.orgChild1?.orgChild1Name, orgChild2Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 - ?.orgChild2Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 + ?.orgChild2Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2?.orgChild2Name, + ?.orgChild2?.orgChild2Name, orgChild3Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 - ?.orgChild3Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 + ?.orgChild3Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3?.orgChild3Name, + ?.orgChild3?.orgChild3Name, orgChild4Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 - ?.orgChild4Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 + ?.orgChild4Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4?.orgChild4Name, + ?.orgChild4?.orgChild4Name, root: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot + ?.orgRootName == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.id, + ?.id, orgChild1: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 - ?.orgChild1Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 + ?.orgChild1Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild1?.id, + ?.orgChild1?.id, orgChild2: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 - ?.orgChild2Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 + ?.orgChild2Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2?.id, + ?.orgChild2?.id, orgChild3: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 - ?.orgChild3Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 + ?.orgChild3Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3?.id, + ?.orgChild3?.id, orgChild4: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 - ?.orgChild4Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 + ?.orgChild4Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4?.id, + ?.orgChild4?.id, rootDna: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot + ?.orgRootName == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.ancestorDNA, + ?.ancestorDNA, orgChild1Dna: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 - ?.orgChild1Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 + ?.orgChild1Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild1?.ancestorDNA, + ?.orgChild1?.ancestorDNA, orgChild2Dna: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 - ?.orgChild2Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 + ?.orgChild2Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2?.ancestorDNA, + ?.orgChild2?.ancestorDNA, orgChild3Dna: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 - ?.orgChild3Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 + ?.orgChild3Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3?.ancestorDNA, + ?.orgChild3?.ancestorDNA, orgChild4Dna: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 - ?.orgChild4Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 + ?.orgChild4Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4?.ancestorDNA, + ?.orgChild4?.ancestorDNA, }; }), ); @@ -10419,64 +10324,64 @@ export class ProfileController extends Controller { findProfile.map(async (item: Profile) => { const posMaster = item.current_holders == null || - item.current_holders.length == 0 || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null + item.current_holders.length == 0 || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id); const position = posMaster == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.positions == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.positions == null || - item.current_holders?.find((x) => x.orgRevisionId == orgRevisionActive.id)?.positions - .length == 0 || - item.current_holders - .find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.positions?.find((position) => position.positionIsSelected == true) == null + item.current_holders?.find((x) => x.orgRevisionId == orgRevisionActive.id)?.positions + .length == 0 || + item.current_holders + .find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.positions?.find((position) => position.positionIsSelected == true) == null ? null : item.current_holders - .find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.positions?.find((position) => position.positionIsSelected == true); + .find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.positions?.find((position) => position.positionIsSelected == true); const posExecutive = position == null || - item.current_holders - .find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == + item.current_holders + .find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == null || - item.current_holders - .find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - ?.posExecutiveName == null + item.current_holders + .find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive + ?.posExecutiveName == null ? null : item.current_holders - .find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - .posExecutiveName; + .find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive + .posExecutiveName; const shortName = item.current_holders.length == 0 ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) != null && - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild4 != null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.orgChild4 != null ? `${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.orgChild4.orgChild4ShortName} ${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) != null && - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild3 != null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.orgChild3 != null ? `${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.orgChild3.orgChild3ShortName} ${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild2 != null + null && + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.orgChild2 != null ? `${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.orgChild2.orgChild2ShortName} ${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgChild1 != null + null && + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.orgChild1 != null ? `${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.orgChild1.orgChild1ShortName} ${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.posMasterNo}` : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) != - null && - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) - ?.orgRoot != null + null && + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id) + ?.orgRoot != null ? `${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive.id)?.posMasterNo}` : null; @@ -10498,54 +10403,54 @@ export class ProfileController extends Controller { isProbation: item.isProbation, orgRootName: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot + ?.orgRootName == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgRoot - ?.orgRootName, + ?.orgRootName, orgChild1Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 - ?.orgChild1Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild1 + ?.orgChild1Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild1?.orgChild1Name, + ?.orgChild1?.orgChild1Name, orgChild2Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 - ?.orgChild2Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild2 + ?.orgChild2Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild2?.orgChild2Name, + ?.orgChild2?.orgChild2Name, orgChild3Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 - ?.orgChild3Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild3 + ?.orgChild3Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild3?.orgChild3Name, + ?.orgChild3?.orgChild3Name, orgChild4Name: item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) == null || + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 == null || - item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 - ?.orgChild4Name == null + item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id)?.orgChild4 + ?.orgChild4Name == null ? null : item.current_holders.find((x) => x.orgRevisionId == orgRevisionActive?.id) - ?.orgChild4?.orgChild4Name, + ?.orgChild4?.orgChild4Name, }; }), ); @@ -10758,14 +10663,14 @@ export class ProfileController extends Controller { } const posExecutive = item.positions == null || - item.positions?.find((position) => position.positionIsSelected == true) == null || - item.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == + item.positions?.find((position) => position.positionIsSelected == true) == null || + item.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == null || - item.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - ?.posExecutiveName == null + item.positions?.find((position) => position.positionIsSelected == true)?.posExecutive + ?.posExecutiveName == null ? null : item.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - .posExecutiveName; + .posExecutiveName; // const amount = // item.current_holder == null || item.current_holder.profileSalary.length == 0 @@ -10844,7 +10749,7 @@ export class ProfileController extends Controller { isLeave: false, isRetired: item.current_holder.birthDate == null || - calculateRetireDate(item.current_holder.birthDate).getFullYear() != body.year + calculateRetireDate(item.current_holder.birthDate).getFullYear() != body.year ? false : true, isSpecial: isSpecial, @@ -10923,98 +10828,98 @@ export class ProfileController extends Controller { position == null || position.posExecutive == null ? null : position.posExecutive.id, rootId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRootId, rootDnaId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.ancestorDNA, root: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgRoot.orgRootName, child1Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1Id, child1DnaId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.ancestorDNA, child1: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild1 - .orgChild1Name, + .orgChild1Name, child2Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2Id, child2DnaId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.ancestorDNA, child2: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild2 - .orgChild2Name, + .orgChild2Name, child3Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3Id, child3DnaId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.ancestorDNA, child3: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild3 - .orgChild3Name, + .orgChild3Name, child4Id: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4Id, child4DnaId: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.ancestorDNA, child4: profile.current_holders == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || - profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null + profile.current_holders.find((x) => x.orgRevisionId == revisionId) == null || + profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 == null ? null : profile.current_holders.find((x) => x.orgRevisionId == revisionId)?.orgChild4 - .orgChild4Name, + .orgChild4Name, }; return new HttpSuccess(_profile); } @@ -11068,67 +10973,67 @@ export class ProfileController extends Controller { const formattedData = profiles.map((item) => { const posMaster = item.current_holders == null || - item.current_holders.length == 0 || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null + item.current_holders.length == 0 || + item.current_holders.find((x) => x.orgRevisionId == findRevision.id) == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id); const position = posMaster == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.positions == null || - item.current_holders?.find((x) => x.orgRevisionId == findRevision.id)?.positions.length == + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.positions == null || + item.current_holders?.find((x) => x.orgRevisionId == findRevision.id)?.positions.length == 0 || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true) == null + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true) == null ? null : item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true); + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true); const posExecutive = position == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - ?.posExecutiveName == null + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive + ?.posExecutiveName == null ? null : item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive - .posExecutiveName; + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.posExecutive + .posExecutiveName; const positionExecutiveField = position == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true) - ?.positionExecutiveField == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true) - ?.positionExecutiveField == null + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true) + ?.positionExecutiveField == null || + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true) + ?.positionExecutiveField == null ? null : item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true) - ?.positionExecutiveField; + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true) + ?.positionExecutiveField; const positionArea = position == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.positionArea == + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.positionArea == null || - item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.positionArea == null + item.current_holders + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.positionArea == null ? null : item.current_holders - .find((x) => x.orgRevisionId == findRevision.id) - ?.positions?.find((position) => position.positionIsSelected == true)?.positionArea; + .find((x) => x.orgRevisionId == findRevision.id) + ?.positions?.find((position) => position.positionIsSelected == true)?.positionArea; const posExecutiveId = position == null || position.posExecutive == null ? null : position.posExecutive.id; @@ -11137,49 +11042,49 @@ export class ProfileController extends Controller { 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 != + 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 != + 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 != + 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 != 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 != null ? `${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` : null; const root = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; const child1 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1; const child2 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2; const child3 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3; const child4 = item.current_holders == null || - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 == null + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 == null ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4; @@ -11305,24 +11210,24 @@ export class ProfileController extends Controller { !profile.current_holders || profile.current_holders.length == 0 ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != - null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3 != + null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild2 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgChild1 != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null + profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) + ?.orgRoot != null ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` : null; let position = @@ -11414,6 +11319,20 @@ export class ProfileController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } + PostRetireToExprofile( + request, + profile.citizenId ?? "", + profile.prefix ?? "", + profile.firstName ?? "", + profile.lastName ?? "", + requestBody.dateLeave?.getFullYear().toString() ?? "", + profile.position, + profile.posType?.posTypeName ?? "", + profile.posLevel?.posLevelName ?? "", + requestBody.dateLeave ?? new Date(), + organizeName, + "ถึงแก่กรรม", + ); return new HttpSuccess(); } @@ -11510,32 +11429,32 @@ export class ProfileController extends Controller { 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 != + 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 != 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 != 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 + 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 + 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; root = item.current_holders.length == 0 || - (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && - item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) + (item.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot == null) ? null : item.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot; root = root == null ? null : root.orgRootName; @@ -12046,90 +11965,4 @@ export class ProfileController extends Controller { return new HttpSuccess(); } - - /** - * API ข้อมูลทะเบียนประวัติตาม keycloak สำหรับเช็คอินเข้าใช้งานระบบ - * - * @summary ข้อมูลทะเบียนประวัติตาม keycloak สำหรับเช็คอินเข้าใช้งานระบบ - * - */ - @Get("keycloak/position-checkin") - async getProfileByKeycloakForCheckin(@Request() request: { user: Record }) { - const userSub = request.user.sub; - const relations = [ - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - ]; - - const [officerProfile, orgRevisionPublish] = await Promise.all([ - this.profileRepo.findOne({ - where: { keycloak: userSub }, - relations, - }), - this.orgRevisionRepo.findOne({ - where: { - orgRevisionIsDraft: false, - orgRevisionIsCurrent: true, - }, - }), - ]); - - if (!orgRevisionPublish) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบแบบร่างโครงสร้าง"); - } - - let profile: any = officerProfile; - let profileType: "OFFICER" | "EMPLOYEE" = "OFFICER"; - - if (!profile) { - profile = await this.profileEmpRepo.findOne({ - where: { keycloak: userSub }, - relations, - }); - profileType = "EMPLOYEE"; - } - - if (!profile) { - if (request.user.role.includes("SUPER_ADMIN")) { - return new HttpSuccess(null); - } - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลบุคคลนี้ในระบบ"); - } - - const currentHolder = - profile.current_holders?.find((x: any) => x.orgRevisionId == orgRevisionPublish.id) ?? null; - const root = currentHolder?.orgRoot ?? null; - const child1 = currentHolder?.orgChild1 ?? null; - const child2 = currentHolder?.orgChild2 ?? null; - const child3 = currentHolder?.orgChild3 ?? null; - const child4 = currentHolder?.orgChild4 ?? null; - - const _profile: any = { - profileId: profile.id, - keycloak: profile.keycloak, - prefix: profile.prefix, - avatar: profile.avatar, - profileType, - isProbation: profile.isProbation, - avatarName: profile.avatarName, - firstName: profile.firstName, - lastName: profile.lastName, - citizenId: profile.citizenId, - root: root?.orgRootName ?? null, - child1: child1?.orgChild1Name ?? null, - child2: child2?.orgChild2Name ?? null, - child3: child3?.orgChild3Name ?? null, - child4: child4?.orgChild4Name ?? null, - privacyCheckin: profile.privacyCheckin, - privacyUser: profile.privacyUser, - privacyMgt: profile.privacyMgt, - ...(profileType !== "OFFICER" ? { type: profile.employeeClass } : {}), - }; - - return new HttpSuccess(_profile); - } } diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index e888a6ae..2a7fba38 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -81,8 +81,9 @@ import { ProfileAssistance } from "../entities/ProfileAssistance"; import { ProfileChangeName } from "../entities/ProfileChangeName"; import { ProfileChildren } from "../entities/ProfileChildren"; import { ProfileDuty } from "../entities/ProfileDuty"; -import { CreatePosMasterHistoryEmployee, getTopDegrees } from "../services/PositionService"; +import { getTopDegrees } from "../services/PositionService"; import { ProfileLeaveService } from "../services/ProfileLeaveService"; +import { PostRetireToExprofile } from "./ExRetirementController"; import { CommandCode } from "../entities/CommandCode"; @Route("api/v1/org/profile-employee") @Tags("ProfileEmployee") @@ -1949,84 +1950,35 @@ export class ProfileEmployeeController extends Controller { // ประวัติพ้นจากราชการ let retires = []; const currentDate = new Date(); + // todo: รอข้อสรุป + // const retire_raw = await this.salaryRepo.findOne({ + // where: { + // profileEmployeeId: id, + // commandCode: In(["12", "15", "16"]), + // }, + // order: { order: "desc" }, + // }); - // commandCode ที่ถือว่าออกจากราชการ - const retireCommandCodes = ["12", "15", "16"]; + // if (retire_raw) { + // const startDate = retire_raw.commandDateAffect; - // ดึงข้อมูล profileSalary ทั้งหมดเพื่อหาประวัติพ้นจากราชการ - const salaries = await this.salaryRepo.find({ - where: { profileEmployeeId: id }, - order: { order: "ASC" }, - }); + // // คำนวณจำนวนวันจากวันพ้นสภาพถึงปัจจุบัน + // let daysCount = 0; + // if (startDate) { + // const start = new Date(startDate); + // daysCount = Math.ceil((currentDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + // } - // มีคำสั่งพ้นราชการหรือไม่ - if ( - salaries.length > 0 && - salaries.some((s) => s.commandCode && retireCommandCodes.includes(s.commandCode)) - ) { - // กรองข้อมูลซ้ำตาม commandDateAffect - const uniqueSalaries = salaries.filter( - (item, index, self) => - index === - self.findIndex( - (t) => t.commandDateAffect?.getTime() === item.commandDateAffect?.getTime(), - ), - ); + // const startDateStr = startDate + // ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) + // : "-"; - // วนลูปหาคู่ของ "ออกราชการ" และ "กลับเข้าราชการ" - for (let i = 0; i < uniqueSalaries.length; i++) { - const current = uniqueSalaries[i]; - - // เป็นคำสั่งออกจากราชการหรือไม่ - if (current.commandCode && retireCommandCodes.includes(current.commandCode)) { - const startDate = current.commandDateAffect; - let endDate: Date | null = null; - let endRecord = null; - - // หาคำสั่งถัดไปที่ไม่ใช่การออกจากราชการ (ถือว่ากลับเข้าราชการ) - for (let j = i + 1; j < uniqueSalaries.length; j++) { - const next = uniqueSalaries[j]; - if (next.commandCode && !retireCommandCodes.includes(next.commandCode)) { - endDate = next.commandDateAffect; - endRecord = next; - break; - } - } - - // ถ้าไม่เจอคำสั่งกลับเข้า ให้ใช้วันปัจจุบัน - if (!endDate) { - endDate = currentDate; - } - - // คำนวณจำนวนวัน - let daysCount = 0; - if (startDate && endDate) { - const start = new Date(startDate); - const end = new Date(endDate); - daysCount = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); - } - - // สร้าง detail จาก commandName + remark - const commandName = current.commandName || ""; - const remark = current.remark || ""; - const detail = `${commandName} ${remark}`.trim(); - - // แปลงวันที่เป็น format ไทย - const startDateStr = startDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(startDate)) - : "-"; - const endDateStr = endDate - ? Extension.ToThaiNumber(Extension.ToThaiFullDate2(endDate)) - : "-"; - - retires.push({ - date: `${startDateStr} - ${endDateStr}`, - detail: detail || "-", - day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-", - }); - } - } - } + // retires.push({ + // date: `${startDateStr} - ปัจจุบัน`, + // detail: retire_raw.commandName ?? "-", + // day: daysCount > 0 ? Extension.ToThaiNumber(daysCount.toLocaleString()) : "-" + // }); + // } // กรณีไม่มีข้อมูล if (retires.length === 0) { @@ -2852,12 +2804,12 @@ export class ProfileEmployeeController extends Controller { queryLike = "profileEmployee.position LIKE :keyword"; } else if (searchField == "posNo") { queryLike = ` - CASE - WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(" ", orgChild4.orgChild4ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(" ", orgChild3.orgChild3ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(" ", orgChild2.orgChild2ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(" ", orgChild1.orgChild1ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - ELSE CONCAT_WS(" ", orgRoot.orgRootShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) + CASE + WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo) + ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo) END LIKE :keyword `; } @@ -3211,12 +3163,12 @@ export class ProfileEmployeeController extends Controller { queryLike = "profileEmployee.position LIKE :keyword"; } else if (searchField == "posNo") { queryLike = ` - CASE - WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT_WS(" ", orgChild4.orgChild4ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT_WS(" ", orgChild3.orgChild3ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT_WS(" ", orgChild2.orgChild2ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT_WS(" ", orgChild1.orgChild1ShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) - ELSE CONCAT_WS(" ", orgRoot.orgRootShortName, current_holders.posMasterNoPrefix, current_holders.posMasterNo, current_holders.posMasterNoSuffix) + CASE + WHEN current_holders.orgChild4Id IS NOT NULL THEN CONCAT(orgChild4.orgChild4ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild3Id IS NOT NULL THEN CONCAT(orgChild3.orgChild3ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild2Id IS NOT NULL THEN CONCAT(orgChild2.orgChild2ShortName, " ", current_holders.posMasterNo) + WHEN current_holders.orgChild1Id IS NOT NULL THEN CONCAT(orgChild1.orgChild1ShortName, " ", current_holders.posMasterNo) + ELSE CONCAT(orgRoot.orgRootShortName, " ", current_holders.posMasterNo) END LIKE :keyword `; } @@ -3368,21 +3320,31 @@ export class ProfileEmployeeController extends Controller { .getManyAndCount(); const data = await Promise.all( record.map((_data) => { - const holder = _data.current_holders.find((x) => x.orgRevisionId == findRevision.id); - const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : ''; - const shortName = !holder - ? null - : holder.orgChild4 != null - ? `${holder.orgChild4.orgChild4ShortName} ${numPart}` - : holder.orgChild3 != null - ? `${holder.orgChild3.orgChild3ShortName} ${numPart}` - : holder.orgChild2 != null - ? `${holder.orgChild2.orgChild2ShortName} ${numPart}` - : holder.orgChild1 != null - ? `${holder.orgChild1.orgChild1ShortName} ${numPart}` - : holder.orgRoot != null - ? `${holder.orgRoot.orgRootShortName} ${numPart}` - : null; + const shortName = + _data.current_holders.length == 0 + ? null + : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + _data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 != + null + ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` + : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild3 != null + ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` + : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild2 != null + ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild2.orgChild2ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` + : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null && + _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgChild1 != null + ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild1.orgChild1ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` + : _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != + null && + _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) + ?.orgRoot != null + ? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgRoot.orgRootShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}` + : null; const dateEmployment = _data.profileEmployeeEmployment.length == 0 ? null @@ -3846,7 +3808,7 @@ export class ProfileEmployeeController extends Controller { holder.orgChild2?.orgChild2ShortName || holder.orgChild1?.orgChild1ShortName || holder.orgRoot?.orgRootShortName; - return `${shortName || ""} ${[holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}`; + return `${shortName || ""} ${holder.posMasterNo || ""}`; }); return profile.current_holders.map((holder, index) => { const position = holder.positions.find((position) => position.posMasterId === holder.id); @@ -5773,9 +5735,6 @@ export class ProfileEmployeeController extends Controller { } await this.profileRepo.save(profile); if (requestBody.isLeave == true) { - if (orgRevisionRef) { - await CreatePosMasterHistoryEmployee(orgRevisionRef.id, request, "DELETE"); - } await removeProfileInOrganize(profile.id, "EMPLOYEE"); } let organizeName = ""; @@ -5789,6 +5748,20 @@ export class ProfileEmployeeController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } + PostRetireToExprofile( + request, + 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(); } diff --git a/src/controllers/ProfileEmployeeTempController.ts b/src/controllers/ProfileEmployeeTempController.ts index a8c017ae..f5182deb 100644 --- a/src/controllers/ProfileEmployeeTempController.ts +++ b/src/controllers/ProfileEmployeeTempController.ts @@ -70,6 +70,7 @@ import { deleteUser } from "../keycloak"; import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; import { getTopDegrees } from "../services/PositionService"; import HttpStatusCode from "../interfaces/http-status"; +import { PostRetireToExprofile } from "./ExRetirementController"; @Route("api/v1/org/profile-temp") @Tags("ProfileEmployee") @Security("bearerAuth") @@ -3607,6 +3608,20 @@ export class ProfileEmployeeTempController extends Controller { ].filter(Boolean); organizeName = names.join(" "); } + PostRetireToExprofile( + request, + 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(); } diff --git a/src/controllers/ProfileGovernmentController.ts b/src/controllers/ProfileGovernmentController.ts index 9af40339..8caaff28 100644 --- a/src/controllers/ProfileGovernmentController.ts +++ b/src/controllers/ProfileGovernmentController.ts @@ -6,6 +6,8 @@ import HttpError from "../interfaces/http-error"; import { RequestWithUser } from "../middlewares/user"; import { Profile } from "../entities/Profile"; import { ProfileGovernment, UpdateProfileGovernment } from "../entities/ProfileGovernment"; +import { Position } from "../entities/Position"; +import { PosMaster } from "../entities/PosMaster"; import { calculateAge, calculateGovAge, @@ -13,6 +15,7 @@ import { setLogDataDiff, } from "../interfaces/utils"; import permission from "../interfaces/permission"; +import { OrgRevision } from "../entities/OrgRevision"; import { In } from "typeorm"; @Route("api/v1/org/profile/government") @Tags("ProfileGovernment") @@ -20,6 +23,9 @@ import { In } from "typeorm"; export class ProfileGovernmentHistoryController extends Controller { private profileRepo = AppDataSource.getRepository(Profile); private govRepo = AppDataSource.getRepository(ProfileGovernment); + private positionRepo = AppDataSource.getRepository(Position); + private posMasterRepo = AppDataSource.getRepository(PosMaster); + private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); /** * * @summary ข้อมูลราชการ @@ -27,6 +33,13 @@ export class ProfileGovernmentHistoryController extends Controller { */ @Get("user") public async getGovHistoryUser(@Request() request: { user: Record }) { + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); const profile = await this.profileRepo.findOneBy({ keycloak: request.user.sub }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); @@ -38,19 +51,79 @@ export class ProfileGovernmentHistoryController extends Controller { posLevel: true, }, }); + const posMaster = await this.posMasterRepo.findOne({ + where: { + // orgRevision: { + // orgRevisionIsCurrent: true, + // orgRevisionIsDraft: false, + // }, + orgRevisionId: orgRevision?.id, + current_holderId: profile.id, + }, + order: { createdAt: "DESC" }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + const position = await this.positionRepo.findOne({ + where: { + positionIsSelected: true, + posMaster: { + // orgRevision: { + // orgRevisionIsCurrent: true, + // orgRevisionIsDraft: false, + // }, + orgRevisionId: orgRevision?.id, + current_holderId: profile.id, + }, + }, + order: { createdAt: "DESC" }, + relations: { + posExecutive: true, + }, + }); + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); - - // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว + const fullNameParts = [ + posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, + posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, + posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, + posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, + posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, + ]; + const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); + let orgShortName = ""; + if (posMaster != null) { + if (posMaster.orgChild1Id === null) { + orgShortName = posMaster.orgRoot?.orgRootShortName; + } else if (posMaster.orgChild2Id === null) { + orgShortName = posMaster.orgChild1?.orgChild1ShortName; + } else if (posMaster.orgChild3Id === null) { + orgShortName = posMaster.orgChild2?.orgChild2ShortName; + } else if (posMaster.orgChild4Id === null) { + orgShortName = posMaster.orgChild3?.orgChild3ShortName; + } else { + orgShortName = posMaster.orgChild4?.orgChild4ShortName; + } + } + //posMaster?.isSit แก้ไขชั่วคราว const data = { - org: record.org ?? null, //สังกัด - positionField: record.positionField ?? null, //สายงาน + org: org, //สังกัด + positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน position: record.position, //ตำแหน่ง 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, //ประเภท - posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร - positionArea: record.positionArea ?? null, //ด้าน/สาขา - positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร + posExecutive: + position == null || position.posExecutive == null || posMaster?.isSit + ? null + : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร + positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา + positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate), dateRetireLaw: record.dateRetireLaw ?? null, // govAge: record.dateStart == null ? null : calculateAge(record.dateStart), @@ -62,10 +135,10 @@ export class ProfileGovernmentHistoryController extends Controller { govAgePlus: record.govAgePlus, reasonSameDate: record.reasonSameDate, }; - + return new HttpSuccess(data); } - + /** * * @summary ข้อมูลราชการ @@ -77,17 +150,25 @@ export class ProfileGovernmentHistoryController extends Controller { let _workflow = await new permission().Workflow(req, profileId, "SYS_REGISTRY_OFFICER"); if (_workflow == false) await new permission().PermissionOrgUserGet(req, "SYS_REGISTRY_OFFICER", profileId); - + + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); + // ค้นหา profile ก่อน const record = await this.profileRepo.findOne({ where: { id: profileId }, relations: ["posType", "posLevel"], }); - + if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile"); } - + // ค้นหา profileSalary แยกต่างหาก const profileWithSalary = await this.profileRepo.findOne({ where: { @@ -120,13 +201,70 @@ export class ProfileGovernmentHistoryController extends Controller { }, }, }); - + // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ record.profileSalary = profileWithSalary?.profileSalary || []; - + const posMaster = await this.posMasterRepo.findOne({ + where: { + orgRevisionId: orgRevision?.id, + current_holderId: profileId, + }, + order: { createdAt: "DESC" }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + const position = await this.positionRepo.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: orgRevision?.id, + current_holderId: profileId, + }, + }, + order: { createdAt: "DESC" }, + relations: { + posExecutive: true, + }, + }); + + // if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const fullNameParts = [ + posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, + posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, + posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, + posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, + posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, + ]; + const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); + let orgShortName = ""; + if (posMaster != null) { + if (posMaster.orgChild1Id === null) { + orgShortName = posMaster.orgRoot?.orgRootShortName ?? ""; + } else if (posMaster.orgChild2Id === null) { + orgShortName = posMaster.orgChild1?.orgChild1ShortName ?? ""; + } else if (posMaster.orgChild3Id === null) { + orgShortName = posMaster.orgChild2?.orgChild2ShortName ?? ""; + } else if (posMaster.orgChild4Id === null) { + orgShortName = posMaster.orgChild3?.orgChild3ShortName ?? ""; + } else { + orgShortName = posMaster.orgChild4?.orgChild4ShortName ?? ""; + } + } let _OrgLeave: any = []; let _profileSalary: any = null; if (record?.isLeave && record?.profileSalary.length > 0) { + // _OrgLeave = [ + // record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null, + // record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null, + // record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null, + // record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null, + // record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null, + // ]; if (record.leaveType == "RETIRE") { _profileSalary = record?.profileSalary.length > 1 @@ -150,23 +288,27 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); - - // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว + //posMaster?.isSit แก้ไขชั่วคราว const data = { - org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด - positionField: record.positionField ?? null, //สายงาน + org: record?.isLeave == false ? org : orgLeave, //สังกัด + positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: record?.isLeave == false - ? record.posMasterNo ?? null + ? posMaster == null + ? null + : `${orgShortName} ${posMaster.posMasterNo}` : _profileSalary != null ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท - posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร - positionArea: record.positionArea ?? null, //ด้าน/สาขา - positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร + posExecutive: + position == null || position.posExecutive == null || posMaster?.isSit + ? null + : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร + positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา + positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), @@ -178,22 +320,30 @@ export class ProfileGovernmentHistoryController extends Controller { govAgePlus: record?.govAgePlus, reasonSameDate: record?.reasonSameDate, }; - + return new HttpSuccess(data); } - + @Get("admin/{profileId}") public async getGovHistoryAdmin(@Path() profileId: string) { + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); + // ค้นหา profile ก่อน const record = await this.profileRepo.findOne({ where: { id: profileId }, relations: ["posType", "posLevel"], }); - + if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล profile"); } - + // ค้นหา profileSalary แยกต่างหาก const profileWithSalary = await this.profileRepo.findOne({ where: { @@ -226,13 +376,70 @@ export class ProfileGovernmentHistoryController extends Controller { }, }, }); - + // ใช้ profileSalary จาก query ที่สอง หรือ [] ถ้าไม่เจอ record.profileSalary = profileWithSalary?.profileSalary || []; - + const posMaster = await this.posMasterRepo.findOne({ + where: { + orgRevisionId: orgRevision?.id, + current_holderId: profileId, + }, + order: { createdAt: "DESC" }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + const position = await this.positionRepo.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: orgRevision?.id, + current_holderId: profileId, + }, + }, + order: { createdAt: "DESC" }, + relations: { + posExecutive: true, + }, + }); + + // if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); + const fullNameParts = [ + posMaster == null || posMaster.orgChild4 == null ? null : posMaster.orgChild4.orgChild4Name, + posMaster == null || posMaster.orgChild3 == null ? null : posMaster.orgChild3.orgChild3Name, + posMaster == null || posMaster.orgChild2 == null ? null : posMaster.orgChild2.orgChild2Name, + posMaster == null || posMaster.orgChild1 == null ? null : posMaster.orgChild1.orgChild1Name, + posMaster == null || posMaster.orgRoot == null ? null : posMaster.orgRoot.orgRootName, + ]; + const org = fullNameParts.filter((part) => part !== undefined && part !== null).join("\n"); + let orgShortName = ""; + if (posMaster != null) { + if (posMaster.orgChild1Id === null) { + orgShortName = posMaster.orgRoot?.orgRootShortName; + } else if (posMaster.orgChild2Id === null) { + orgShortName = posMaster.orgChild1?.orgChild1ShortName; + } else if (posMaster.orgChild3Id === null) { + orgShortName = posMaster.orgChild2?.orgChild2ShortName; + } else if (posMaster.orgChild4Id === null) { + orgShortName = posMaster.orgChild3?.orgChild3ShortName; + } else { + orgShortName = posMaster.orgChild4?.orgChild4ShortName; + } + } let _OrgLeave: any = []; let _profileSalary: any = null; if (record?.isLeave && record?.profileSalary.length > 0) { + // _OrgLeave = [ + // record?.profileSalary[0].orgChild4 ? record?.profileSalary[0].orgChild4 : null, + // record?.profileSalary[0].orgChild3 ? record?.profileSalary[0].orgChild3 : null, + // record?.profileSalary[0].orgChild2 ? record?.profileSalary[0].orgChild2 : null, + // record?.profileSalary[0].orgChild1 ? record?.profileSalary[0].orgChild1 : null, + // record?.profileSalary[0].orgRoot ? record?.profileSalary[0].orgRoot : null, + // ]; if (record.leaveType == "RETIRE") { _profileSalary = record?.profileSalary.length > 1 @@ -256,23 +463,27 @@ export class ProfileGovernmentHistoryController extends Controller { } } const orgLeave = _OrgLeave.filter((x: any) => x !== undefined && x !== null).join("\n"); - - // ดึงข้อมูลจาก profile ที่เก็บไว้แล้ว + //posMaster?.isSit แก้ไขชั่วคราว const data = { - org: record?.isLeave == false ? (record.org ?? null) : orgLeave, //สังกัด - positionField: record.positionField ?? null, //สายงาน + org: record?.isLeave == false ? org : orgLeave, //สังกัด + positionField: position == null || posMaster?.isSit ? null : position.positionField, //สายงาน position: record?.position, //ตำแหน่ง posLevel: record?.posLevel == null ? null : record?.posLevel.posLevelName, //ระดับ posMasterNo: record?.isLeave == false - ? record.posMasterNo ?? null + ? posMaster == null + ? null + : `${orgShortName} ${posMaster.posMasterNo}` : _profileSalary != null ? `${_profileSalary.posNoAbb} ${_profileSalary.posNo}` : null, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท - posExecutive: record.posExecutive ?? null, //ตำแหน่งทางการบริหาร - positionArea: record.positionArea ?? null, //ด้าน/สาขา - positionExecutiveField: record.positionExecutiveField ?? null, //ด้านทางการบริหาร + posExecutive: + position == null || position.posExecutive == null || posMaster?.isSit + ? null + : position.posExecutive.posExecutiveName, //ตำแหน่งทางการบริหาร + positionArea: position == null || posMaster?.isSit ? null : position.positionArea, //ด้าน/สาขา + positionExecutiveField: position == null || posMaster?.isSit ? null : position.positionExecutiveField, //ด้านทางการบริหาร dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), dateRetireLaw: record?.dateRetireLaw ?? null, // govAge: record?.dateStart == null ? null : calculateAge(record?.dateStart), @@ -285,10 +496,10 @@ export class ProfileGovernmentHistoryController extends Controller { reasonSameDate: record?.reasonSameDate, isLeave: record?.isLeave, }; - + return new HttpSuccess(data); } - + /** * * @summary ประวัติข้อมูลราชการ by keycloak @@ -306,7 +517,7 @@ export class ProfileGovernmentHistoryController extends Controller { }); return new HttpSuccess(record); } - + /** * * @summary ประวัติข้อมูลราชการ @@ -322,12 +533,12 @@ export class ProfileGovernmentHistoryController extends Controller { order: { lastUpdatedAt: "DESC" }, where: { profileId: profileId }, }); - + // record.pop(); - + return new HttpSuccess(record); } - + /** * * @summary แก้ไขข้อมูลราชการ @@ -343,14 +554,14 @@ export class ProfileGovernmentHistoryController extends Controller { const record = await this.profileRepo.findOne({ where: { id: profileId }, }); - + if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล"); const before = structuredClone(record); const history = new ProfileGovernment(); - + Object.assign(record, body); Object.assign(history, { ...record, id: undefined }); - + history.profileId = profileId; record.lastUpdateUserId = req.user.sub; record.lastUpdateFullName = req.user.name; @@ -361,14 +572,13 @@ export class ProfileGovernmentHistoryController extends Controller { history.createdFullName = req.user.name; history.createdAt = new Date(); history.lastUpdatedAt = new Date(); - + await Promise.all([ this.profileRepo.save(record, { data: req }), setLogDataDiff(req, { before, after: record }), this.govRepo.save(history, { data: req }), ]); - + return new HttpSuccess(); } } - \ No newline at end of file diff --git a/src/controllers/ProfileGovernmentEmployeeController.ts b/src/controllers/ProfileGovernmentEmployeeController.ts index 7fdeb9a7..6709e5db 100644 --- a/src/controllers/ProfileGovernmentEmployeeController.ts +++ b/src/controllers/ProfileGovernmentEmployeeController.ts @@ -115,7 +115,7 @@ export class ProfileGovernmentEmployeeController extends Controller { record.posType == null && record.posLevel == null ? null : `${record.posType.posTypeShortName} ${record.posLevel.posLevelName}`, //ระดับ - posMasterNo: posMaster == null ? null : `${orgShortName} ${[posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}`, //เลขที่ตำแหน่ง + posMasterNo: posMaster == null ? null : `${orgShortName} ${posMaster.posMasterNo}`, //เลขที่ตำแหน่ง posType: record.posType == null ? null : record.posType.posTypeName, //ประเภท dateLeave: record.birthDate == null ? null : calculateRetireDate(record.birthDate), dateRetireLaw: record.dateRetireLaw ?? null, @@ -281,9 +281,9 @@ export class ProfileGovernmentEmployeeController extends Controller { record?.isLeave == false ? posMaster == null ? null - : `${orgShortName} ${[posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}` - : posNoLeave /*record && record?.profileSalary.length > 0 - ? `${record?.profileSalary[0].posNoAbb} ${record?.profileSalary[0].posNo}` + : `${orgShortName} ${posMaster.posMasterNo}` + : posNoLeave /*record && record?.profileSalary.length > 0 + ? `${record?.profileSalary[0].posNoAbb} ${record?.profileSalary[0].posNo}` : null*/, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), //วันเกษียณ @@ -441,9 +441,9 @@ export class ProfileGovernmentEmployeeController extends Controller { record?.isLeave == false ? posMaster == null ? null - : `${orgShortName} ${[posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ')}` - : posNoLeave /*record && record.profileSalary.length > 0 - ? `${record?.profileSalary[0].posNoAbb} ${record?.profileSalary[0].posNo}` + : `${orgShortName} ${posMaster.posMasterNo}` + : posNoLeave /*record && record.profileSalary.length > 0 + ? `${record?.profileSalary[0].posNoAbb} ${record?.profileSalary[0].posNo}` : null*/, //เลขที่ตำแหน่ง posType: record?.posType == null ? null : record?.posType.posTypeName, //ประเภท dateLeave: record?.birthDate == null ? null : calculateRetireDate(record?.birthDate), //วันเกษียณ diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index 19a9f5e4..4736337a 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -23,21 +23,11 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; import { In, IsNull, LessThan, MoreThan, Not } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; -import { normalizeDurationSumSimple } from "../utils/tenure"; -import { - TenurePositionOfficer, - CreateTenurePositionOfficer, -} from "../entities/TenurePositionOfficer"; -import { TenureLevelOfficer, CreateTenureLevelOfficer } from "../entities/TenureLevelOfficer"; -import { - TenurePositionEmployee, - CreateTenurePositionEmployee, -} from "../entities/TenurePositionEmployee"; -import { TenureLevelEmployee, CreateTenureLevelEmployee } from "../entities/TenureLevelEmployee"; -import { - TenurePositionExecutiveOfficer, - CreateTenurePositionExecutiveOfficer, -} from "../entities/TenurePositionExecutiveOfficer"; +import { TenurePositionOfficer } from "../entities/TenurePositionOfficer"; +import { TenureLevelOfficer } from "../entities/TenureLevelOfficer"; +import { TenurePositionEmployee } from "../entities/TenurePositionEmployee"; +import { TenureLevelEmployee } from "../entities/TenureLevelEmployee"; +import { TenurePositionExecutiveOfficer } from "../entities/TenurePositionExecutiveOfficer"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; import { OrgRevision } from "../entities/OrgRevision"; @@ -55,303 +45,138 @@ export class ProfileSalaryController extends Controller { private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee); private salaryRepo = AppDataSource.getRepository(ProfileSalary); private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); + private positionOfficerRepo = AppDataSource.getRepository(TenurePositionOfficer); + private positionEmployeeRepo = AppDataSource.getRepository(TenurePositionEmployee); + private levelOfficerRepo = AppDataSource.getRepository(TenureLevelOfficer); + private levelEmployeeRepo = AppDataSource.getRepository(TenureLevelEmployee); + private positionExecutiveOfficerRepo = AppDataSource.getRepository( + TenurePositionExecutiveOfficer, + ); private commandRepository = AppDataSource.getRepository(Command); private orgRootRepository = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); + private positionRepo = AppDataSource.getRepository(Position); private registryRepo = AppDataSource.getRepository(Registry); private registryEmployeeRepo = AppDataSource.getRepository(RegistryEmployee); @Get("TenurePositionOfficer") public async cronjobTenurePositionOfficer() { + let data: any = []; + await this.positionOfficerRepo.clear(); + const profile = await this.profileRepo.find(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - const baseCurrentDate = CURRENT_DATE[0].today; - - const profiles = await this.profileRepo.find({ - select: ["id", "position", "isLeave", "leaveDate"], - where: { position: Not(IsNull()) }, - }); - - const BATCH_SIZE = 100; - let successCount = 0; - let failCount = 0; - const allData: CreateTenurePositionOfficer[] = []; - - for (let i = 0; i < profiles.length; i += BATCH_SIZE) { - const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); - const results = await Promise.allSettled( - batch.map((profile) => - this.processSingleProfileForTenurePositionOfficer(profile, baseCurrentDate), - ), - ); - - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - allData.push(result.value); - successCount++; - } else { - failCount++; - } - }); - } - - await AppDataSource.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(TenurePositionOfficer, {}); - if (allData.length > 0) { - const entities = allData.map((data) => { - const entity = new TenurePositionOfficer(); - Object.assign(entity, data); - return entity; - }); - await transactionalEntityManager.save(TenurePositionOfficer, entities); + let _currentDate = CURRENT_DATE[0].today; + for await (const x of profile) { + if (x.isLeave) { + _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; } - }); - - return new HttpSuccess({ - message: `อัปเดต tenure position officer สำเร็จ`, - total: profiles.length, - success: successCount, - failed: failCount, - }); - } - - private async processSingleProfileForTenurePositionOfficer( - profile: Pick, - baseCurrentDate: string, - ): Promise { - try { - let _currentDate = baseCurrentDate; - if (profile.isLeave && profile.leaveDate) { - _currentDate = Extension.toDateOnlyString(profile.leaveDate); - } - const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [ - profile.id, + 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, - year: - curr.Years !== null && curr.Years !== undefined - ? Math.floor(Number(curr.Years)) - : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: - curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - - const currentTenure = mapPosition.find((curr: any) => curr.positionName === profile.position); - - if (currentTenure) { - const normalized = normalizeDurationSumSimple( - currentTenure.year, - currentTenure.month, - currentTenure.day, + 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 }, ); - return { - profileId: profile.id, - positionName: currentTenure.positionName, - days_diff: null, - Years: normalized.years, - Months: normalized.months, - Days: normalized.days, - }; - } - return null; - } catch (error) { - return null; + const mapData: any = { + profileId: x.id, + positionName: calDayDiff.positionName, + days_diff: calDayDiff.days_diff, + // Years: (calDayDiff.days_diff / 365.2524).toFixed(4), + // Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), + // Days: (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: Math.floor(calDayDiff.days_diff / 365.2524), + Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12), + Days: Math.floor(calDayDiff.days_diff % 30.4375), + }; + // data.push(_mapData); + await this.positionOfficerRepo.save(mapData); } + // await this.positionOfficerRepo.save(data); + + return new HttpSuccess(); } @Get("TenurePositionEmployee") public async cronjobTenurePositionEmployee() { + let data: any = []; + await this.positionEmployeeRepo.clear(); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - const baseCurrentDate = CURRENT_DATE[0].today; - - const profiles = await this.profileEmployeeRepo.find({ - select: ["id", "position", "isLeave", "leaveDate"], - where: { position: Not(IsNull()) }, - }); - - const BATCH_SIZE = 100; - let successCount = 0; - let failCount = 0; - const allData: CreateTenurePositionEmployee[] = []; - - for (let i = 0; i < profiles.length; i += BATCH_SIZE) { - const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); - const results = await Promise.allSettled( - batch.map((profile) => - this.processSingleProfileForTenurePositionEmployee(profile, baseCurrentDate), - ), - ); - - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - allData.push(result.value); - successCount++; - } else { - failCount++; - } - }); - } - - await AppDataSource.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(TenurePositionEmployee, {}); - if (allData.length > 0) { - const entities = allData.map((data) => { - const entity = new TenurePositionEmployee(); - Object.assign(entity, data); - return entity; - }); - await transactionalEntityManager.save(TenurePositionEmployee, entities); + let _currentDate = CURRENT_DATE[0].today; + const profile = await this.profileEmployeeRepo.find(); + for await (const x of profile) { + if (x?.isLeave) { + _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; } - }); - - return new HttpSuccess({ - message: `อัปเดต tenure position employee สำเร็จ`, - total: profiles.length, - success: successCount, - failed: failCount, - }); - } - - private async processSingleProfileForTenurePositionEmployee( - profile: Pick, - baseCurrentDate: string, - ): Promise { - try { - let _currentDate = baseCurrentDate; - if (profile.isLeave && profile.leaveDate) { - _currentDate = Extension.toDateOnlyString(profile.leaveDate); - } - const position = await AppDataSource.query("CALL GetProfileEmployeeSalaryPosition(?, ?)", [ - profile.id, + 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, - year: - curr.Years !== null && curr.Years !== undefined - ? Math.floor(Number(curr.Years)) - : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: - curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - - const currentTenure = mapPosition.find((curr: any) => curr.positionName === profile.position); - - if (currentTenure) { - const normalized = normalizeDurationSumSimple( - currentTenure.year, - currentTenure.month, - currentTenure.day, + 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 }, ); - return { - profileEmployeeId: profile.id, - positionName: currentTenure.positionName, - days_diff: null, - Years: normalized.years, - Months: normalized.months, - Days: normalized.days, - }; - } - return null; - } catch (error) { - return null; + const mapData: any = { + profileEmployeeId: x.id, + positionName: calDayDiff.positionName, + days_diff: calDayDiff.days_diff, + // Years: (calDayDiff.days_diff / 365.2524).toFixed(4), + // Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), + // Days: (calDayDiff.days_diff % 30.4375).toFixed(4), + Years: Math.floor(calDayDiff.days_diff / 365.2524), + Months: Math.floor((calDayDiff.days_diff / 30.4375) % 12), + Days: Math.floor(calDayDiff.days_diff % 30.4375), + }; + // data.push(_mapData); + await this.positionEmployeeRepo.save(mapData); } + // await this.positionEmployeeRepo.save(data); + + return new HttpSuccess(); } @Get("TenureLevelOfficer") public async cronjobTenureLevelOfficer() { + let data: any = []; + await this.levelOfficerRepo.clear(); + const profile = await this.profileRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - const baseCurrentDate = CURRENT_DATE[0].today; - - const profiles = await this.profileRepo.find({ - relations: ["posLevel", "posType"], - select: ["id", "isLeave", "leaveDate", "posLevel", "posType"], - where: { - posLevel: Not(IsNull()), - posType: Not(IsNull()), - }, - }); - - const BATCH_SIZE = 100; - let successCount = 0; - let failCount = 0; - const allData: CreateTenureLevelOfficer[] = []; - - for (let i = 0; i < profiles.length; i += BATCH_SIZE) { - const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); - const results = await Promise.allSettled( - batch.map((profile) => - this.processSingleProfileForTenureLevelOfficer(profile, baseCurrentDate), - ), - ); - - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - allData.push(result.value); - successCount++; - } else { - failCount++; - } - }); - } - - await AppDataSource.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(TenureLevelOfficer, {}); - if (allData.length > 0) { - const entities = allData.map((data) => { - const entity = new TenureLevelOfficer(); - Object.assign(entity, data); - return entity; - }); - await transactionalEntityManager.save(TenureLevelOfficer, entities); + let _currentDate = CURRENT_DATE[0].today; + for await (const x of profile) { + if (x?.isLeave) { + _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; } - }); - - return new HttpSuccess({ - message: `อัปเดต tenure level officer สำเร็จ`, - total: profiles.length, - success: successCount, - failed: failCount, - }); - } - - private async processSingleProfileForTenureLevelOfficer( - profile: Pick & { - posLevel?: { posLevelName?: string } | null; - posType?: { posTypeName?: string } | null; - }, - baseCurrentDate: string, - ): Promise { - try { - let _currentDate = baseCurrentDate; - if (profile.isLeave && profile.leaveDate) { - _currentDate = Extension.toDateOnlyString(profile.leaveDate); - } - const positionLevel = await AppDataSource.query("CALL GetProfileSalaryLevel(?, ?)", [ - profile.id, + x.id, _currentDate, ]); const _positionLevel = positionLevel.length > 0 ? positionLevel[0] : []; - const mapPositionLevel = _positionLevel.length > 1 ? _positionLevel.slice(1).map((curr: any, index: number) => ({ @@ -359,24 +184,13 @@ export class ProfileSalaryController extends Controller { positionType: _positionLevel[index]?.positionType, positionLevel: _positionLevel[index]?.positionLevel, positionCee: _positionLevel[index]?.positionCee, - year: - curr.Years !== null && curr.Years !== undefined - ? Math.floor(Number(curr.Years)) - : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: - curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - const calDayDiff = mapPositionLevel .filter( (curr: any) => - curr.positionLevel === (profile.posLevel?.posLevelName ?? null) && - curr.positionType === (profile.posType?.posTypeName ?? null), + curr.positionLevel == (x.posLevel?.posLevelName ?? null) && + curr.positionType == (x.posType?.posTypeName ?? null), ) .reduce( (acc: any, curr: any) => { @@ -384,118 +198,43 @@ export class ProfileSalaryController extends Controller { acc.positionType = curr.positionType; acc.positionLevel = curr.positionLevel; acc.positionCee = curr.positionCee; - acc.year += curr.year; - acc.month += curr.month; - acc.day += curr.day; return acc; }, - { - days_diff: 0, - positionType: null, - positionLevel: null, - positionCee: null, - year: 0, - month: 0, - day: 0, - }, + { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, ); - - const normalized = normalizeDurationSumSimple( - calDayDiff.year, - calDayDiff.month, - calDayDiff.day, - ); - - return { - profileId: profile.id, + const mapData: any = { + profileId: x.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: profile.posLevel == null ? 0 : normalized.years, - Months: profile.posLevel == null ? 0 : normalized.months, - Days: profile.posLevel == null ? 0 : normalized.days, + Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4), + Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), + Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4), }; - } catch (error) { - return null; + // data.push(_mapData); + await this.levelOfficerRepo.save(mapData); } + // await this.levelOfficerRepo.save(data); + + return new HttpSuccess(); } @Get("TenureLevelEmployee") public async cronjobTenureLevelEmployee() { + let data: any = []; + await this.levelEmployeeRepo.clear(); + const profile = await this.profileEmployeeRepo.find({ relations: ["posLevel", "posType"] }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - const baseCurrentDate = CURRENT_DATE[0].today; - - const profiles = await this.profileEmployeeRepo.find({ - relations: ["posLevel", "posType"], - select: ["id", "isLeave", "leaveDate", "posLevel", "posType"], - where: { - posLevel: Not(IsNull()), - posType: Not(IsNull()), - }, - }); - - const BATCH_SIZE = 100; - let successCount = 0; - let failCount = 0; - const allData: CreateTenureLevelEmployee[] = []; - - for (let i = 0; i < profiles.length; i += BATCH_SIZE) { - const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); - const results = await Promise.allSettled( - batch.map((profile) => - this.processSingleProfileForTenureLevelEmployee(profile, baseCurrentDate), - ), - ); - - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - allData.push(result.value); - successCount++; - } else { - failCount++; - } - }); - } - - await AppDataSource.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(TenureLevelEmployee, {}); - if (allData.length > 0) { - const entities = allData.map((data) => { - const entity = new TenureLevelEmployee(); - Object.assign(entity, data); - return entity; - }); - await transactionalEntityManager.save(TenureLevelEmployee, entities); + let _currentDate = CURRENT_DATE[0].today; + for await (const x of profile) { + if (x?.isLeave) { + _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; } - }); - - return new HttpSuccess({ - message: `อัปเดต tenure level employee สำเร็จ`, - total: profiles.length, - success: successCount, - failed: failCount, - }); - } - - private async processSingleProfileForTenureLevelEmployee( - profile: Pick & { - posLevel?: { posLevelName?: string } | null; - posType?: { posTypeName?: string } | null; - }, - baseCurrentDate: string, - ): Promise { - try { - let _currentDate = baseCurrentDate; - if (profile.isLeave && profile.leaveDate) { - _currentDate = Extension.toDateOnlyString(profile.leaveDate); - } - const positionLevel = await AppDataSource.query("CALL GetProfileEmployeeSalaryLevel(?, ?)", [ - profile.id, + x.id, _currentDate, ]); const _positionLevel = positionLevel.length > 0 ? positionLevel[0] : []; - const mapPositionLevel = _positionLevel.length > 1 ? _positionLevel.slice(1).map((curr: any, index: number) => ({ @@ -503,24 +242,13 @@ export class ProfileSalaryController extends Controller { positionType: _positionLevel[index]?.positionType, positionLevel: _positionLevel[index]?.positionLevel, positionCee: _positionLevel[index]?.positionCee, - year: - curr.Years !== null && curr.Years !== undefined - ? Math.floor(Number(curr.Years)) - : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: - curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - const calDayDiff = mapPositionLevel .filter( (curr: any) => - curr.positionLevel === (profile.posLevel?.posLevelName ?? null) && - curr.positionType === (profile.posType?.posTypeName ?? null), + curr.positionLevel == (x.posLevel?.posLevelName ?? null) && + curr.positionType == (x.posType?.posTypeName ?? null), ) .reduce( (acc: any, curr: any) => { @@ -528,161 +256,92 @@ export class ProfileSalaryController extends Controller { acc.positionType = curr.positionType; acc.positionLevel = curr.positionLevel; acc.positionCee = curr.positionCee; - acc.year += curr.year; - acc.month += curr.month; - acc.day += curr.day; return acc; }, - { - days_diff: 0, - positionType: null, - positionLevel: null, - positionCee: null, - year: 0, - month: 0, - day: 0, - }, + { days_diff: 0, positionType: null, positionLevel: null, positionCee: null }, ); - - const normalized = normalizeDurationSumSimple( - calDayDiff.year, - calDayDiff.month, - calDayDiff.day, - ); - - return { - profileEmployeeId: profile.id, + const mapData: any = { + profileEmployeeId: x.id, positionType: calDayDiff.positionType, positionLevel: calDayDiff.positionLevel, positionCee: calDayDiff.positionCee, days_diff: calDayDiff.days_diff, - Years: profile.posLevel == null ? 0 : normalized.years, - Months: profile.posLevel == null ? 0 : normalized.months, - Days: profile.posLevel == null ? 0 : normalized.days, + Years: x.posLevel == null ? 0 : (calDayDiff.days_diff / 365.2524).toFixed(4), + Months: x.posLevel == null ? 0 : ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), + Days: x.posLevel == null ? 0 : (calDayDiff.days_diff % 30.4375).toFixed(4), }; - } catch (error) { - return null; + // data.push(_mapData); + await this.levelEmployeeRepo.save(mapData); } + // await this.levelEmployeeRepo.save(data); + + return new HttpSuccess(); } @Get("TenurePositionExecutiveOfficer") public async cronjobTenureExecutivePositionOfficer() { + await this.positionExecutiveOfficerRepo.clear(); + const profile = await this.profileRepo.find(); + const orgRevision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { + orgRevisionIsDraft: false, + orgRevisionIsCurrent: true, + }, + }); const CURRENT_DATE = await AppDataSource.query("SELECT CURRENT_DATE() as today"); - const baseCurrentDate = CURRENT_DATE[0].today; - - const profiles = await this.profileRepo.find({ - select: ["id", "posExecutive", "isLeave", "leaveDate"], - where: { posExecutive: Not(IsNull()) }, - }); - - const BATCH_SIZE = 100; - let successCount = 0; - let failCount = 0; - const allData: CreateTenurePositionExecutiveOfficer[] = []; - - for (let i = 0; i < profiles.length; i += BATCH_SIZE) { - const batch = profiles.slice(i, Math.min(i + BATCH_SIZE, profiles.length)); - const results = await Promise.allSettled( - batch.map((profile) => - this.processSingleProfileForTenureExecutivePositionOfficer(profile, baseCurrentDate), - ), - ); - - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - allData.push(result.value); - successCount++; - } else { - failCount++; - } + let _currentDate = CURRENT_DATE[0].today; + for await (const x of profile) { + if (x?.isLeave) { + _currentDate = x.leaveDate ? Extension.toDateOnlyString(x.leaveDate) : _currentDate; + } + const position = await this.positionRepo.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: orgRevision?.id, + current_holderId: x.id, + }, + }, + order: { createdAt: "DESC" }, + relations: { + posExecutive: true, + }, }); - } - - await AppDataSource.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(TenurePositionExecutiveOfficer, {}); - if (allData.length > 0) { - const entities = allData.map((data) => { - const entity = new TenurePositionExecutiveOfficer(); - Object.assign(entity, data); - return entity; - }); - await transactionalEntityManager.save(TenurePositionExecutiveOfficer, entities); - } - }); - - return new HttpSuccess({ - message: `อัปเดต tenure executive position officer สำเร็จ`, - total: profiles.length, - success: successCount, - failed: failCount, - }); - } - - private async processSingleProfileForTenureExecutivePositionOfficer( - profile: Pick, - baseCurrentDate: string, - ): Promise { - try { - let _currentDate = baseCurrentDate; - if (profile.isLeave && profile.leaveDate) { - _currentDate = Extension.toDateOnlyString(profile.leaveDate); - } - const positionExecutive = await AppDataSource.query("CALL GetProfileSalaryExecutive(?, ?)", [ - profile.id, + x.id, _currentDate, ]); const _position = positionExecutive.length > 0 ? positionExecutive[0] : []; - const mapPosition = _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days_diff: curr.days_diff, positionExecutive: _position[index]?.positionExecutive, - year: - curr.Years !== null && curr.Years !== undefined - ? Math.floor(Number(curr.Years)) - : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: - curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, })) : []; - + const _posExecutiveName = position?.posExecutive?.posExecutiveName; const calDayDiff = mapPosition - .filter((curr: any) => curr.positionExecutive === profile.posExecutive) + .filter((curr: any) => _posExecutiveName && curr.positionExecutive == _posExecutiveName) .reduce( (acc: any, curr: any) => { acc.days_diff += Number(curr.days_diff) || 0; acc.positionExecutive = curr.positionExecutive; - acc.year += curr.year; - acc.month += curr.month; - acc.day += curr.day; return acc; }, - { days_diff: 0, positionExecutive: null, year: 0, month: 0, day: 0 }, + { days_diff: 0, positionExecutive: null }, ); - - const normalized = normalizeDurationSumSimple( - calDayDiff.year, - calDayDiff.month, - calDayDiff.day, - ); - - return { - profileId: profile.id, + const mapData: any = { + profileId: x.id, positionExecutiveName: calDayDiff.positionExecutive, days_diff: calDayDiff.days_diff, - Years: normalized.years, - Months: normalized.months, - Days: normalized.days, + Years: (calDayDiff.days_diff / 365.2524).toFixed(4), + Months: ((calDayDiff.days_diff / 30.4375) % 12).toFixed(4), + Days: (calDayDiff.days_diff % 30.4375).toFixed(4), }; - } catch (error) { - return null; + await this.positionExecutiveOfficerRepo.save(mapData); } + return new HttpSuccess(); } @Get("Registry") @@ -928,14 +587,6 @@ export class ProfileSalaryController extends Controller { _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) : []; @@ -946,25 +597,15 @@ export class ProfileSalaryController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + // Recalculate year, month, and day + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -979,14 +620,6 @@ export class ProfileSalaryController extends Controller { _posLevel.length > 1 ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee ? `ระดับ ${_posLevel[index]?.positionCee.trim()}` @@ -1003,25 +636,15 @@ export class ProfileSalaryController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + // Recalculate year, month, and day + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -1037,14 +660,6 @@ export class ProfileSalaryController extends Controller { _posExecutive.length > 1 ? _posExecutive.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _posExecutive[index]?.positionExecutive, })) : []; @@ -1055,25 +670,15 @@ export class ProfileSalaryController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + // Recalculate year, month, and day + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -1116,14 +721,9 @@ export class ProfileSalaryController extends Controller { _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, + // year: curr.Years ? Math.floor(Number(curr.Years)) : 0, + // month: curr.Months ? Math.floor(Number(curr.Months)) : 0, + // day: curr.Days ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) : []; @@ -1134,25 +734,15 @@ export class ProfileSalaryController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + // Recalculate year, month, and day + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -1168,14 +758,9 @@ export class ProfileSalaryController extends Controller { _posLevel.length > 1 ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, + // year: curr.Years ? Math.floor(Number(curr.Years)) : 0, + // month: curr.Months ? Math.floor(Number(curr.Months)) : 0, + // day: curr.Days ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee ? `ระดับ ${_posLevel[index]?.positionCee.trim()}` @@ -1192,25 +777,15 @@ export class ProfileSalaryController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + // Recalculate year, month, and day + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -1225,15 +800,10 @@ export class ProfileSalaryController extends Controller { const mapPosExecutive = _posExecutive.length > 1 ? _posExecutive.slice(1).map((curr: any, index: number) => ({ + // year: curr.Years ? Math.floor(Number(curr.Years)) : 0, + // month: curr.Months ? Math.floor(Number(curr.Months)) : 0, + // day: curr.Days ? Math.floor(Number(curr.Days)) : 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: _posExecutive[index]?.positionExecutive, })) : []; @@ -1244,25 +814,15 @@ export class ProfileSalaryController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + // Recalculate year, month, and day + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -1351,17 +911,6 @@ export class ProfileSalaryController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(data, { ...body, ...meta }); - // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect - if (["12", "15", "16"].includes(body.commandCode ?? "")) { - data.isGovernment = false; - if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; - } - // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect - else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { - data.isGovernment = true; - if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; - } - const history = new ProfileSalaryHistory(); Object.assign(history, { ...data, id: undefined }); await this.salaryRepo.save(data, { data: req }); @@ -1486,17 +1035,6 @@ export class ProfileSalaryController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(record, body); - // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect - if (["12", "15", "16"].includes(body.commandCode ?? "")) { - record.isGovernment = false; - if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; - } - // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect - else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { - record.isGovernment = true; - if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; - } - Object.assign(history, { ...record, id: undefined }); history.profileSalaryId = salaryId; diff --git a/src/controllers/ProfileSalaryEmployeeController.ts b/src/controllers/ProfileSalaryEmployeeController.ts index 5f2ea997..f113a2ba 100644 --- a/src/controllers/ProfileSalaryEmployeeController.ts +++ b/src/controllers/ProfileSalaryEmployeeController.ts @@ -27,7 +27,6 @@ import { Profile } from "../entities/Profile"; import { In, LessThan, IsNull, MoreThan } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; -import { normalizeDurationSumSimple } from "../utils/tenure"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; import Extension from "../interfaces/extension"; @@ -161,14 +160,6 @@ export class ProfileSalaryEmployeeController extends Controller { _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) : []; @@ -179,25 +170,14 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -213,14 +193,6 @@ export class ProfileSalaryEmployeeController extends Controller { _posLevel.length > 1 ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee ? `ระดับ ${_posLevel[index]?.positionCee.trim()}` @@ -234,25 +206,14 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -290,14 +251,6 @@ export class ProfileSalaryEmployeeController extends Controller { _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: _position[index]?.positionName, })) : []; @@ -308,25 +261,14 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -342,14 +284,6 @@ export class ProfileSalaryEmployeeController extends Controller { _posLevel.length > 1 ? _posLevel.slice(1).map((curr: any, index: number) => ({ days: curr.days_diff ? Number(curr.days_diff) : 0, - // Use stored procedure's calculated values (calendar arithmetic) - year: - curr.Years !== null && curr.Years !== undefined ? Math.floor(Number(curr.Years)) : 0, - month: - curr.Months !== null && curr.Months !== undefined - ? Math.floor(Number(curr.Months)) - : 0, - day: curr.Days !== null && curr.Days !== undefined ? Math.floor(Number(curr.Days)) : 0, name: !_posLevel[index]?.positionType && _posLevel[index]?.positionCee ? `ระดับ ${_posLevel[index]?.positionCee.trim()}` @@ -363,25 +297,14 @@ export class ProfileSalaryEmployeeController extends Controller { if (existing) { existing.days += curr.days; - existing.year += curr.year; - existing.month += curr.month; - existing.day += curr.day; } else { - existing = { - name: curr.name, - days: curr.days, - year: curr.year, - month: curr.month, - day: curr.day, - }; + existing = { name: curr.name, days: curr.days }; acc.push(existing); } - // Normalize the summed values using calendar arithmetic - const normalized = normalizeDurationSumSimple(existing.year, existing.month, existing.day); - existing.year = normalized.years; - existing.month = normalized.months; - existing.day = normalized.days; + existing.year = Math.floor(existing.days / 365.2524); + existing.month = Math.floor((existing.days / 30.4375) % 12); + existing.day = Math.floor(existing.days % 30.4375); return acc; }, @@ -475,17 +398,6 @@ export class ProfileSalaryEmployeeController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(data, { ...body, ...meta }); - // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect - if (["12", "15", "16"].includes(body.commandCode ?? "")) { - data.isGovernment = false; - if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; - } - // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect - else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { - data.isGovernment = true; - if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; - } - const history = new ProfileSalaryHistory(); Object.assign(history, { ...data, id: undefined }); const _null: any = null; @@ -620,16 +532,6 @@ export class ProfileSalaryEmployeeController extends Controller { else if (body.commandCode == "19") body.commandName = "ไม่ได้เลื่อนเงินเดือน/ค่าจ้าง"; } Object.assign(record, body); - // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect - if (["12", "15", "16"].includes(body.commandCode ?? "")) { - record.isGovernment = false; - if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null; - } - // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect - else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { - record.isGovernment = true; - if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect ?? null; - } Object.assign(history, { ...record, id: undefined }); history.profileSalaryId = salaryId; diff --git a/src/controllers/ProfileSalaryTempController.ts b/src/controllers/ProfileSalaryTempController.ts index 35279fbc..fc6a9df5 100644 --- a/src/controllers/ProfileSalaryTempController.ts +++ b/src/controllers/ProfileSalaryTempController.ts @@ -133,8 +133,8 @@ export class ProfileSalaryTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` - : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `current_holders.orgChild1Id is null` + // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -545,8 +545,8 @@ export class ProfileSalaryTempController extends Controller { _data.child1 != undefined && _data.child1 != null ? _data.child1[0] != null ? `current_holders.orgChild1Id IN (:...child1)` - : // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - `current_holders.orgChild1Id is null` + // : `current_holders.orgChild1Id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : `current_holders.orgChild1Id is null` : "1=1", { child1: _data.child1, @@ -1233,13 +1233,6 @@ export class ProfileSalaryTempController extends Controller { isDelete: false, }; Object.assign(data, { ...body, ...meta }); - // if (["12", "15", "16"].includes(body.commandCode ?? "")) { - // data.isGovernment = false; - // if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; - // } else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { - // data.isGovernment = true; - // if (body.commandDateAffect) data.dateGovernment = body.commandDateAffect; - // } await this.salaryRepo.save(data, { data: req }); setLogDataDiff(req, { before, after: data }); @@ -1440,10 +1433,10 @@ export class ProfileSalaryTempController extends Controller { profileEmployeeId: x.profileEmployeeId, dateStart: x.commandDateAffect, dateEnd: null, - posNo: `${x.posNoAbb ?? ""} ${x.posNo ?? ""}`.trim(), + posNo: `${x.posNoAbb} ${x.posNo}`, position: x.positionName, commandId: x.commandId, - refCommandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined, + refCommandNo: `${x.commandNo}/${x.commandYear}`, refCommandDate: x.commandDateAffect, status: false, isDeleted: false, @@ -1463,7 +1456,7 @@ export class ProfileSalaryTempController extends Controller { dateStart: x.commandDateAffect, dateEnd: null, commandId: x.commandId, - commandNo: [x.commandNo, x.commandYear].filter(Boolean).join("/") || undefined, + commandNo: `${x.commandNo}/${x.commandYear}`, commandName: x.commandName ?? "ให้ช่วยราชการ", refCommandDate: x.commandDateSign, refId: x.refId, @@ -1516,16 +1509,6 @@ export class ProfileSalaryTempController extends Controller { const before = structuredClone(record); Object.assign(record, body); - // 12,15,16 isGovernment = false & dateGovernment = commandDateAffect - if (["12", "15", "16"].includes(body.commandCode ?? "")) { - record.isGovernment = false; - if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; - } - // 1,2,3,4,10,11,20 isGovernment = true & dateGovernment = commandDateAffect - else if (["1", "2", "3", "4", "10", "11", "20"].includes(body.commandCode ?? "")) { - record.isGovernment = true; - if (body.commandDateAffect) record.dateGovernment = body.commandDateAffect; - } record.isEdit = true; record.lastUpdateUserId = req.user.sub; diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts index 0494be98..aa6908e2 100644 --- a/src/controllers/ScriptProfileOrgController.ts +++ b/src/controllers/ScriptProfileOrgController.ts @@ -38,10 +38,6 @@ export class ScriptProfileOrgController extends Controller { process.env.CRONJOB_UPDATE_WINDOW_HOURS || "24", 10, ); - private readonly LEAVE_SERVICE_BATCH_SIZE = parseInt( - process.env.LEAVE_SERVICE_BATCH_SIZE || "50", - 10, - ); /** * Script to update profile's organizational structure in leave service and sync to Keycloak @@ -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 */ @Post("update-org") - public async cronjobUpdateOrg(@Request() _request: RequestWithUser) { + public async cronjobUpdateOrg(@Request() request: RequestWithUser) { // Idempotency check - prevent concurrent runs if (this.isRunning) { console.log("cronjobUpdateOrg: Job already running, skipping this execution"); @@ -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 const profileIdsByType = this.groupProfileIdsByType(payloads); @@ -245,90 +256,16 @@ export class ScriptProfileOrgController extends Controller { syncResults.failed += typeResult.failed; } - // Update profile's org structure in leave service by calling API - console.log("cronjobUpdateOrg: Calling leave service API with chunking", { - payloadCount: payloads.length, - batchSize: this.LEAVE_SERVICE_BATCH_SIZE, - expectedBatches: Math.ceil(payloads.length / this.LEAVE_SERVICE_BATCH_SIZE), - }); - - const chunks = this.chunkArray(payloads, this.LEAVE_SERVICE_BATCH_SIZE); - const leaveServiceResults = { - total: payloads.length, - success: 0, - failed: 0, - batchesCompleted: 0, - batchesFailed: 0, - }; - - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - const batchNumber = i + 1; - - console.log( - `cronjobUpdateOrg: Processing leave service batch ${batchNumber}/${chunks.length}`, - { - batchSize: chunk.length, - batchRange: `${i * this.LEAVE_SERVICE_BATCH_SIZE + 1}-${Math.min( - (batchNumber + 1) * this.LEAVE_SERVICE_BATCH_SIZE, - payloads.length, - )}`, - }, - ); - - try { - await axios.put( - `${process.env.API_URL}/leave-beginning/schedule/update-dna`, - chunk, - { - headers: { - "Content-Type": "application/json", - api_key: process.env.API_KEY, - }, - timeout: 120000, // 120 second timeout per chunk - }, - ); - - leaveServiceResults.success += chunk.length; - leaveServiceResults.batchesCompleted++; - - console.log(`cronjobUpdateOrg: Leave service batch ${batchNumber}/${chunks.length} completed`, { - success: chunk.length, - }); - } catch (error: any) { - leaveServiceResults.failed += chunk.length; - leaveServiceResults.batchesFailed++; - - console.error( - `cronjobUpdateOrg: Leave service batch ${batchNumber}/${chunks.length} failed`, - { - error: error.message, - batchSize: chunk.length, - responseStatus: error.response?.status, - responseData: error.response?.data, - }, - ); - - // Continue processing remaining batches - } - } - - console.log("cronjobUpdateOrg: Leave service API call completed", { - ...leaveServiceResults, - }); - const duration = Date.now() - startTime; console.log("cronjobUpdateOrg: Job completed", { duration: `${duration}ms`, processed: payloads.length, - leaveServiceResults, syncResults, }); return new HttpSuccess({ message: "Update org completed", processed: payloads.length, - leaveServiceResults, syncResults, duration: `${duration}ms`, }); diff --git a/src/controllers/SocketController.ts b/src/controllers/SocketController.ts index 13bdde1d..eb0b72c2 100644 --- a/src/controllers/SocketController.ts +++ b/src/controllers/SocketController.ts @@ -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 { RequestWithUser } from "../middlewares/user"; @Route("/api/v1/org/through-socket") 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, - ); - } } diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 4902ce0f..afc686e6 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -580,27 +580,18 @@ export class KeycloakController extends Controller { new Brackets((qb) => { qb.orWhere( body.keyword != null && body.keyword != "" - ? `profile.citizenId LIKE :keyword` + ? `profile.citizenId like '%${body.keyword}%'` : "1=1", - { - keyword: `%${body.keyword}%`, - } ) .orWhere( body.keyword != null && body.keyword != "" - ? `profile.email LIKE :keyword` + ? `profile.email like '%${body.keyword}%'` : "1=1", - { - keyword: `%${body.keyword}%`, - } ) .orWhere( 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", - { - keyword: `%${body.keyword}%`, - } ); }), ) @@ -634,27 +625,18 @@ export class KeycloakController extends Controller { new Brackets((qb) => { qb.orWhere( body.keyword != null && body.keyword != "" - ? `profileEmployee.citizenId LIKE :keyword` + ? `profileEmployee.citizenId like '%${body.keyword}%'` : "1=1", - { - keyword: `%${body.keyword}%`, - } ) .orWhere( body.keyword != null && body.keyword != "" - ? `profileEmployee.email LIKE :keyword` + ? `profileEmployee.email like '%${body.keyword}%'` : "1=1", - { - keyword: `%${body.keyword}%`, - } ) .orWhere( 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", - { - keyword: `%${body.keyword}%`, - } ); }), ) @@ -832,68 +814,6 @@ export class KeycloakController extends Controller { if (!result) throw new Error("Failed. Cannot remove group to user."); } - @Post("user/reset-password") - @Security("bearerAuth", ["admin"]) - async resetUserPassword(@Request() req: RequestWithUser, @Body() body: { keycloak: string }) { - if (!req.user.role.includes("ADMIN") && !req.user.role.includes("SUPER_ADMIN")) { - throw new HttpError(HttpStatus.FORBIDDEN, "ไม่มีสิทธิ์ดำเนินการ"); - } - - let profile: Profile | ProfileEmployee | null = await this.profileRepo.findOne({ - where: { keycloak: body.keycloak }, - select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"], - }); - - let isEmployee = false; - if (!profile) { - profile = await this.profileEmpRepo.findOne({ - where: { keycloak: body.keycloak, employeeClass: "PERM" }, - select: ["id", "keycloak", "birthDate", "firstName", "lastName", "citizenId"], - }); - isEmployee = true; - } - - if (!profile) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้"); - } - - if (!profile.keycloak) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ผู้ใช้ไม่ได้เชื่อมต่อกับ Keycloak"); - } - - let newPassword: string; - const isProduction = process.env.NODE_ENV === "production"; - - if (isProduction && profile.birthDate) { - const _date = new Date(profile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; - newPassword = `${_date}${_month}${_year}`; - } else { - newPassword = "P@ssw0rd"; - } - - const result = await changeUserPassword(profile.keycloak, newPassword); - if (!result) { - throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "ไม่สามารถรีเซ็ตรหัสผ่านได้"); - } - - addLogSequence(req, { - action: "reset-password", - status: "success", - description: `รีเซ็ตรหัสผ่านสำหรับ ${profile.firstName} ${profile.lastName} (${profile.citizenId})`, - }); - - const response = new HttpSuccess(); - response.message = "รีเซ็ตรหัสผ่านสำเร็จ"; - return response; - } - @Get("user/role/{id}") async getRoleUser(@Request() req: RequestWithUser, @Path("id") id: string) { const profile = await this.profileRepo.findOne({ diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index ae5c8859..0609c932 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -23,7 +23,6 @@ import { viewDirector } from "../entities/view/viewDirector"; import { ProfileEmployee } from "../entities/ProfileEmployee"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { OrgRoot } from "../entities/OrgRoot"; -import { getPosMasterPositions } from "../services/PositionService"; @Route("api/v1/org/workflow") @Tags("Workflow") @Security("bearerAuth") @@ -238,21 +237,11 @@ export class WorkflowController extends Controller { savedStates.find((state) => state.id === so.stateId && state.order === 1), ); - // add link sysName = REGISTRY_PROFILE or REGISTRY_PROFILE_EMP - let notiLink = ""; - if (body.sysName === "REGISTRY_PROFILE") { - notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`; - } else if (body.sysName === "REGISTRY_PROFILE_EMP") { - notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`; - } else if (body.sysName === "REGISTRY_IDP") { - notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`; - } - const notificationReceivers = stateOperatorUsersToCreate .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) .map((user) => ({ receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, - notiLink: notiLink, + notiLink: "", })); // ส่ง notification แบบ fire-and-forget @@ -915,14 +904,14 @@ export class WorkflowController extends Controller { const roodIds = [posMasterUser.orgRootId]; const orgRoot = await this.orgRootRepo.findOne({ select: { id: true, isDeputy: true }, - where: { + where: { id: Not(posMasterUser.orgRootId), isDeputy: true, orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }, }); if (orgRoot && orgRoot.isDeputy) { - roodIds.push(orgRoot.id); + roodIds.push(orgRoot.id) } // 2. Pre-calculate conditions - ย้ายออกมาข้างนอก @@ -1072,48 +1061,12 @@ export class WorkflowController extends Controller { ]); // 8. ปรับ response mapping (ถ้าจำเป็น) - let posMasterPositionMap: Map = new Map(); - - 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, - positionSign: newPositionSign, - posExecutiveNameOrg: - (x.posExecutiveName ?? "") + - (x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""), - }; - }); + const processedData = data.map((x: any) => ({ + ...x, + posExecutiveNameOrg: + (x.posExecutiveName ?? "") + + (x.orgChild4 ?? x.orgChild3 ?? x.orgChild2 ?? x.orgChild1 ?? x.orgRoot ?? ""), + })); return new HttpSuccess({ data: processedData, total }); } diff --git a/src/entities/Issues.ts b/src/entities/Issues.ts index dc5dbc33..aff597e1 100644 --- a/src/entities/Issues.ts +++ b/src/entities/Issues.ts @@ -38,11 +38,11 @@ export class Issues extends EntityBase { @Column({ type: "enum", - enum: ["NEW", "IN_PROGRESS", "RESOLVED", "CLOSED", "HELPDESK_IN_PROGRESS", "REPLIED"], + enum: ["NEW", "IN_PROGRESS", "RESOLVED", "CLOSED"], default: "NEW", comment: "สถานะการแก้ไขปัญหา", }) - status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; + status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED"; @BeforeInsert() async generateCodeIssue() { @@ -77,7 +77,7 @@ export interface IssueResponse { menu: string | null; org: string | null; remark: string | null; - status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; + status: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED"; createdAt: Date; lastUpdatedAt: Date; createdFullName: string; @@ -90,7 +90,7 @@ export interface CreateIssueRequest { title: string; description?: string; system: string; - status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; + status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED"; menu?: string; org?: string; email?: string; @@ -98,6 +98,6 @@ export interface CreateIssueRequest { } export interface UpdateIssueRequest { - status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED" | "HELPDESK_IN_PROGRESS" | "REPLIED"; + status?: "NEW" | "IN_PROGRESS" | "RESOLVED" | "CLOSED"; remark?: string; } diff --git a/src/entities/PosMasterEmployeeHistory.ts b/src/entities/PosMasterEmployeeHistory.ts index e0aa7853..b0418644 100644 --- a/src/entities/PosMasterEmployeeHistory.ts +++ b/src/entities/PosMasterEmployeeHistory.ts @@ -99,51 +99,51 @@ export class PosMasterEmployeeHistory extends EntityBase { }) ancestorDNA: string; - @Column({ - nullable: true, - length: 40, - comment: "คีย์นอก(FK)ของตาราง profileEmployee", - default: null, - }) - profileEmployeeId: string; + // @Column({ + // nullable: true, + // length: 40, + // comment: "คีย์นอก(FK)ของตาราง profile", + // default: null, + // }) + // profileId: string; - @Column({ - nullable: true, - length: 40, - comment: "dna ของตาราง orgRoot", - default: null, - }) - rootDnaId: string; + // @Column({ + // nullable: true, + // length: 40, + // comment: "dna ของตาราง orgRoot", + // default: null, + // }) + // rootDnaId: string; - @Column({ - nullable: true, - length: 40, - comment: "dna ของตาราง orgChild1", - default: null, - }) - child1DnaId: string; + // @Column({ + // nullable: true, + // length: 40, + // comment: "dna ของตาราง orgChild1", + // default: null, + // }) + // child1DnaId: string; - @Column({ - nullable: true, - length: 40, - comment: "dna ของตาราง orgChild2", - default: null, - }) - child2DnaId: string; + // @Column({ + // nullable: true, + // length: 40, + // comment: "dna ของตาราง orgChild2", + // default: null, + // }) + // child2DnaId: string; - @Column({ - nullable: true, - length: 40, - comment: "dna ของตาราง orgChild3", - default: null, - }) - child3DnaId: string; + // @Column({ + // nullable: true, + // length: 40, + // comment: "dna ของตาราง orgChild3", + // default: null, + // }) + // child3DnaId: string; - @Column({ - nullable: true, - length: 40, - comment: "dna ของตาราง orgChild4", - default: null, - }) - child4DnaId: string; + // @Column({ + // nullable: true, + // length: 40, + // comment: "dna ของตาราง orgChild4", + // default: null, + // }) + // child4DnaId: string; } diff --git a/src/entities/Profile.ts b/src/entities/Profile.ts index a875a969..72a1d505 100644 --- a/src/entities/Profile.ts +++ b/src/entities/Profile.ts @@ -140,54 +140,6 @@ export class Profile extends EntityBase { }) posTypeId: string | null; - @Column({ - nullable: true, - comment: "สายงาน", - length: 45, - default: null, - }) - positionField: string; - - @Column({ - nullable: true, - comment: "ตำแหน่งทางการบริหาร", - length: 255, - default: null, - }) - posExecutive?: string; - - @Column({ - nullable: true, - comment: "ด้าน/สาขา", - length: 255, - default: null, - }) - positionArea?: string; - - @Column({ - nullable: true, - comment: "ด้านทางการบริหาร", - length: 255, - default: null, - }) - positionExecutiveField?: string; - - @Column({ - nullable: true, - comment: "เลขที่ตำแหน่ง", - length: 255, - default: null, - }) - posMasterNo?: string; - - @Column({ - nullable: true, - comment: "สังกัด", - type: "text", - default: null, - }) - org?: string; - @Column({ nullable: true, length: 255, diff --git a/src/entities/TenureLevelEmployee.ts b/src/entities/TenureLevelEmployee.ts index 5654e306..36ae0176 100644 --- a/src/entities/TenureLevelEmployee.ts +++ b/src/entities/TenureLevelEmployee.ts @@ -74,7 +74,7 @@ export class TenureLevelEmployee extends EntityBase { positionLevel: string; } -export class CreateTenureLevelEmployee { +export class CreateTenureLevelOfficer { profileEmployeeId: string; positionCee: string | null; days_diff: number | null; diff --git a/src/entities/view/viewDirectorActing.ts b/src/entities/view/viewDirectorActing.ts index a9c8b096..ecdb09a5 100644 --- a/src/entities/view/viewDirectorActing.ts +++ b/src/entities/view/viewDirectorActing.ts @@ -81,8 +81,6 @@ export class viewDirectorActing { @ViewColumn() posNo: string; @ViewColumn() - posNoAct: string; - @ViewColumn() posLevel: string; @ViewColumn() posType: string; @@ -128,6 +126,4 @@ export class viewDirectorActing { key: string; @ViewColumn() positionSign: string; - @ViewColumn() - positionSignChild: string; } diff --git a/src/keycloak/index.ts b/src/keycloak/index.ts index b59d5e81..b661450c 100644 --- a/src/keycloak/index.ts +++ b/src/keycloak/index.ts @@ -116,34 +116,6 @@ export async function withRetry( 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 { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - try { - const response = await fetch(url, { - ...options, - signal: controller.signal, - }); - clearTimeout(timeoutId); - return response; - } catch (error: any) { - clearTimeout(timeoutId); - if (error.name === "AbortError") { - throw new Error(`Request timeout after ${timeout}ms`); - } - throw error; - } -} - const KC_URL = process.env.KC_URL; const KC_REALMS = process.env.KC_REALMS; const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID; @@ -172,12 +144,10 @@ export function isTokenExpired(token: string, beforeExpire: number = 30) { /** * Get token from keycloak if needed - * Returns null if Keycloak is unavailable */ -export async function getToken(): Promise { +export async function getToken() { if (!KC_CLIENT_ID || !KC_SECRET) { - console.error("[getToken] KC_CLIENT_ID and KC_SECRET are required"); - return null; + throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature."); } if (token && !isTokenExpired(token)) return token; @@ -188,35 +158,22 @@ export async function getToken(): Promise { body.append("client_secret", KC_SECRET); body.append("grant_type", "client_credentials"); - try { - const res = await fetchWithTimeout( - `${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, - { - method: "POST", - body: body, - }, - 10000, - ); + const res = await fetch(`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, { + method: "POST", + body: body, + }).catch((e) => console.error(e)); - if (!res.ok) { - console.error(`[getToken] Keycloak token request failed: ${res.status}`); - return null; - } - - const data = (await res.json()) as any; - - if (data && data.access_token) { - token = data.access_token; - console.log(`[getToken] Token refreshed successfully`); - return token; - } - - console.error("[getToken] No access_token in response"); - return null; - } catch (error: any) { - console.error(`[getToken] Failed to get token: ${error.message}`); - return null; + if (!res) { + throw new Error("Cannot get token from keycloak."); } + + const data = (await res.json()) as any; + + if (data && data.access_token) { + token = data.access_token; + } + console.log(`token: ${token}`); + return token; } /** @@ -232,16 +189,10 @@ export async function createUser( opts?: Record, token?: string, ) { - const authToken = token || (await getToken()); - if (!authToken) { - console.error("[createUser] Failed to get Keycloak token"); - return false; - } - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, { // prettier-ignore headers: { - "authorization": `Bearer ${authToken}`, + "authorization": `Bearer ${token || await getToken()}`, "content-type": `application/json`, }, method: "POST", @@ -255,6 +206,7 @@ export async function createUser( if (!res) return false; if (!res.ok) { + // return Boolean(console.error("Keycloak Error Response: ", await res.json())); return await res.json(); } @@ -271,16 +223,10 @@ export async function createUser( * @returns user if success, false otherwise. */ export async function getUser(userId: string, token?: string) { - const authToken = token || (await getToken()); - if (!authToken) { - console.error("[getUser] Failed to get Keycloak token"); - return false; - } - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${authToken}`, + "authorization": `Bearer ${token || await getToken()}`, "content-type": `application/json`, }, }).catch((e) => console.log("Keycloak Error: ", e)); @@ -299,16 +245,10 @@ export async function getUser(userId: string, token?: string) { * @returns user if success, false otherwise. */ export async function getUserByUsername(citizenId: string, token?: string) { - const authToken = token || (await getToken()); - if (!authToken) { - console.error("[getUserByUsername] Failed to get Keycloak token"); - return false; - } - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${authToken}`, + "authorization": `Bearer ${token || await getToken()}`, "content-type": `application/json`, }, }).catch((e) => console.log("Keycloak Error: ", e)); @@ -439,38 +379,23 @@ export async function getUserCountOrg(first = "", max = "", search = "", userIds export async function editUser(userId: string, opts: Record) { const { password, ...rest } = opts; - const token = await getToken(); - if (!token) { - console.error("[editUser] Failed to get Keycloak token"); - return false; - } - - // Get existing user data to preserve other fields - const existingUser = await getUser(userId, token); - if (!existingUser) { - console.error(`[editUser] User ${userId} not found in Keycloak`); - return false; - } - - // Merge existing user data with updated fields - const updatedUser = { - ...existingUser, - ...rest, - credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, - }; - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${token}`, + "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, 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)); if (!res) return false; if (!res.ok) { + // return Boolean(console.error("Keycloak Error Response: ", await res.json())); return await res.json(); } @@ -494,24 +419,6 @@ export async function updateName( ) { // const { password, ...rest } = opts; - // Get existing user data to preserve other fields - const existingUser = await getUser(userId); - if (!existingUser) { - console.error(`[updateName] User ${userId} not found in Keycloak`); - return false; - } - - // Merge existing user data with updated name fields - const updatedUser = { - ...existingUser, - firstName, - lastName, - attributes: { - ...(existingUser.attributes || {}), - prefix, - }, - }; - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { @@ -519,7 +426,16 @@ export async function updateName( "content-type": `application/json`, }, 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)); if (!res) return false; @@ -570,16 +486,10 @@ export async function enableStatus(userId: string, status: boolean) { * @returns user true if success, false otherwise. */ export async function deleteUser(userId: string, token?: string) { - const authToken = token || (await getToken()); - if (!authToken) { - console.error("[deleteUser] Failed to get Keycloak token"); - return false; - } - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { - "authorization": `Bearer ${authToken}`, + "authorization": `Bearer ${token || await getToken()}`, "content-type": `application/json`, }, method: "DELETE", @@ -961,16 +871,10 @@ export async function removeUserGroup(userId: string, groupId: string) { // Function to change user password export async function changeUserPassword(userId: string, newPassword: string) { try { - const token = await getToken(); - if (!token) { - console.error("[changeUserPassword] Failed to get Keycloak token"); - return false; - } - const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, { // prettier-ignore headers: { - "authorization": `Bearer ${token}`, + "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "PUT", @@ -981,15 +885,6 @@ export async function changeUserPassword(userId: string, newPassword: string) { }), }).catch((e) => console.log("Keycloak Error: ", e)); - if (!res) { - console.error("[changeUserPassword] No response from Keycloak"); - return false; - } - if (!res.ok) { - console.error(`[changeUserPassword] Failed to change password: ${res.status}`); - return false; - } - return true; } catch (error) { console.error("Error changing password:", error); @@ -1000,61 +895,60 @@ export async function changeUserPassword(userId: string, newPassword: string) { // Function to reset password export async function resetPassword(username: string) { try { - const token = await getToken(); - if (!token) { - console.error("[resetPassword] Failed to get Keycloak token"); - return false; - } + // if (!API_KEY || !AUTH_ACCOUNT_SECRET) { + // throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature."); + // } + // const body = new URLSearchParams(); + // body.append("client_id", "gettoken"); + // body.append("client_secret", AUTH_ACCOUNT_SECRET?.toString()); + // body.append("grant_type", "client_credentials"); + // const tokenResponse = await fetch(`${process.env.KC_URL}/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, { + // method: "POST", + // headers: { + // "Content-Type": "application/x-www-form-urlencoded", + // api_key: API_KEY, + // }, + // body: body + // }); + // if (!tokenResponse.ok) { + // throw new Error("Failed to get admin token"); + // } + // const tokenData = await tokenResponse.json(); + // const adminToken = tokenData.access_token; - const users = await fetchWithTimeout( + const users = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`, { headers: { - authorization: `Bearer ${token}`, + authorization: `Bearer ${await getToken()}`, + // "authorization": `Bearer ${adminToken}`, "content-type": `application/json`, }, }, - 10000, ); - if (!users.ok) { - const errorText = await users.text(); - console.error(`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`); return false; } - const usersData = await users.json(); - - if (!usersData || usersData.length === 0) { - console.error(`[resetPassword] User not found with email: ${username}`); - return false; - } - const userId = usersData[0].id; - - const resetResponse = await fetchWithTimeout( + const resetResponse = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`, { method: "PUT", headers: { Authorization: `Bearer ${await getToken()}`, + // "Authorization": `Bearer ${adminToken}`, "Content-Type": "application/json", }, body: JSON.stringify(["UPDATE_PASSWORD"]), }, - 10000, ); - if (!resetResponse.ok) { - const errorText = await resetResponse.text(); - console.error(`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`); return false; } - - console.log(`[resetPassword] Password reset email sent successfully to: ${username}`); return { message: "Password reset email sent" }; - } catch (error: any) { - console.error(`[resetPassword] Error triggering password reset: ${error.message}`); + } catch (error) { + console.error("Error triggering password reset:", error); return false; } } @@ -1064,14 +958,8 @@ export async function updateUserAttributes( attributes: Record, ): Promise { try { - const token = await getToken(); - if (!token) { - console.error("[updateUserAttributes] Failed to get Keycloak token"); - return false; - } - // Get existing user data to preserve other attributes - const existingUser = await getUser(userId, token); + const existingUser = await getUser(userId); if (!existingUser) { console.error(`User ${userId} not found in Keycloak`); @@ -1096,7 +984,7 @@ export async function updateUserAttributes( const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { headers: { - authorization: `Bearer ${token}`, + authorization: `Bearer ${await getToken()}`, "content-type": "application/json", }, method: "PUT", diff --git a/src/middlewares/authWebService.ts b/src/middlewares/authWebService.ts index 1f17b9cf..fa50b3fe 100644 --- a/src/middlewares/authWebService.ts +++ b/src/middlewares/authWebService.ts @@ -17,17 +17,7 @@ export async function handleWebServiceAuth(request: express.Request) { // ตรวจสอบ API Key กับฐานข้อมูล const apiKeyData = await AppDataSource.getRepository(ApiKey).findOne({ - select: { - id: true, - name: true, - keyApi: true, - accessType: true, - dnaRootId: true, - dnaChild1Id: true, - dnaChild2Id: true, - dnaChild3Id: true, - dnaChild4Id: true, - }, + select: { id: true, name: true, keyApi: true }, where: { keyApi: apiKey }, relations: ["apiNames"], }); @@ -50,12 +40,6 @@ export async function handleWebServiceAuth(request: express.Request) { name: apiKeyData.name, type: "web-service", accessApi: apiKeyData.apiNames.map((x) => x.id) ?? [], - accessType: apiKeyData.accessType, - dnaRootId: apiKeyData.dnaRootId, - dnaChild1Id: apiKeyData.dnaChild1Id, - dnaChild2Id: apiKeyData.dnaChild2Id, - dnaChild3Id: apiKeyData.dnaChild3Id, - dnaChild4Id: apiKeyData.dnaChild4Id, }; } diff --git a/src/middlewares/logs.ts b/src/middlewares/logs.ts index 391e8699..3f1a5963 100644 --- a/src/middlewares/logs.ts +++ b/src/middlewares/logs.ts @@ -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-employee/")) 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/commandSys/")) system = "admin"; @@ -80,17 +79,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { // 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 === 2 && res.statusCode < 400) return; if (level === 3 && res.statusCode < 200) return; @@ -106,7 +94,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) { method: req.method, endpoint: req.url, responseCode: String(res.statusCode === 304 ? 200 : res.statusCode), - responseDescription: _msg, + responseDescription: data?.message, input: level === 4 ? JSON.stringify(req.body, null, 2) : undefined, output: level === 4 ? JSON.stringify(data, null, 2) : undefined, ...req.app.locals.logData, diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index 09e32ef9..75c84d01 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -25,11 +25,5 @@ export type RequestWithUserWebService = Request & { id: string; name: string; accessApi: string[]; - accessType?: string; - dnaRootId?: string | null; - dnaChild1Id?: string | null; - dnaChild2Id?: string | null; - dnaChild3Id?: string | null; - dnaChild4Id?: string | null; }; }; diff --git a/src/migration/1776308026834-add_position_fields_to_profile.ts b/src/migration/1776308026834-add_position_fields_to_profile.ts deleted file mode 100644 index 9b214460..00000000 --- a/src/migration/1776308026834-add_position_fields_to_profile.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddPositionFieldsToProfile1776308026834 implements MigrationInterface { - name = 'AddPositionFieldsToProfile1776308026834' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - 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\``); - } -} diff --git a/src/migration/1778208324657-add_status_enum_to_issues.ts b/src/migration/1778208324657-add_status_enum_to_issues.ts deleted file mode 100644 index 60b2dc79..00000000 --- a/src/migration/1778208324657-add_status_enum_to_issues.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddStatusEnumToIssues1778208324657 implements MigrationInterface { - name = 'AddStatusEnumToIssues1778208324657' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - await queryRunner.query(`ALTER TABLE \`issues\` CHANGE \`status\` \`status\` enum ('NEW', 'IN_PROGRESS', 'RESOLVED', 'CLOSED') NOT NULL COMMENT 'สถานะการแก้ไขปัญหา' DEFAULT 'NEW'`); - } -} diff --git a/src/migration/1779244154610-update_posMasterEmpHis_add_dna.ts b/src/migration/1779244154610-update_posMasterEmpHis_add_dna.ts deleted file mode 100644 index 5b7a4a1d..00000000 --- a/src/migration/1779244154610-update_posMasterEmpHis_add_dna.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class UpdatePosMasterEmpHisAddDna1779244154610 implements MigrationInterface { - name = 'UpdatePosMasterEmpHisAddDna1779244154610' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - 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\``); - } -} diff --git a/src/scripts/ClearOldOrgRevision.ts b/src/scripts/ClearOldOrgRevision.ts deleted file mode 100644 index 4d2be7a7..00000000 --- a/src/scripts/ClearOldOrgRevision.ts +++ /dev/null @@ -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 { - 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(); diff --git a/src/services/ActingPositionService.ts b/src/services/ActingPositionService.ts deleted file mode 100644 index e5a0b601..00000000 --- a/src/services/ActingPositionService.ts +++ /dev/null @@ -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 { - // ดึงข้อมูล 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 = {}; - - 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); - } - } - - // จัดรูปแบบเลขที่ตำแหน่งตามรูปแบบ 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(); diff --git a/src/services/ClearOldOrgRevisionService.ts b/src/services/ClearOldOrgRevisionService.ts deleted file mode 100644 index 056206f3..00000000 --- a/src/services/ClearOldOrgRevisionService.ts +++ /dev/null @@ -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 { - 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( - manager: EntityManager, - entity: EntityTarget, - field: keyof Entity, - ids: string[], -): Promise { - if (ids.length === 0) { - return 0; - } - - const alias = "entity"; - return manager - .createQueryBuilder(entity, alias) - .where(`${alias}.${String(field)} IN (:...ids)`, { ids }) - .getCount(); -} diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index 7bfe88ed..2b3af9ab 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -442,223 +442,6 @@ export class KeycloakAttributeService { } } - /** - * Check if Keycloak user has empty/null empType attribute - * @param keycloakUserId - Keycloak user ID - * @returns Object with isEmpty flag and currentEmpType value - */ - async checkEmpTypeEmpty(keycloakUserId: string): Promise<{ - isEmpty: boolean; - currentEmpType?: string; - }> { - try { - const user = await getUser(keycloakUserId); - - if (!user || !user.attributes) { - return { isEmpty: true }; - } - - const empType = user.attributes.empType?.[0]; - - return { - isEmpty: !empType || empType.trim() === "", - currentEmpType: empType || "", - }; - } catch (error) { - console.error(`[checkEmpTypeEmpty] Error for user ${keycloakUserId}:`, error); - return { isEmpty: true }; // Assume empty on error - } - } - - /** - * Sync profiles with missing empType for a specific month - * @param options - Sync configuration - * @returns Sync results summary - */ - async syncMissingEmpTypeByMonth(options: { - month: string; // "YYYY-MM" format - profileType?: "PROFILE" | "PROFILE_EMPLOYEE"; - dryRun?: boolean; - concurrency?: number; - rateLimit?: number; - }): Promise<{ - month: string; - profileType: string; - totalProfiles: number; - profilesChecked: number; - missingEmpType: number; - syncSuccess: number; - syncFailed: number; - skipped: number; - executionTime: string; - dryRun: boolean; - }> { - const startTime = Date.now(); - const { - month, - profileType = "PROFILE", - dryRun = false, - concurrency = 5, - rateLimit = 10, - } = options; - - const result = { - month, - profileType, - totalProfiles: 0, - profilesChecked: 0, - missingEmpType: 0, - syncSuccess: 0, - syncFailed: 0, - skipped: 0, - executionTime: "", - dryRun, - }; - - let rateLimiter: RateLimiter | null = null; - - try { - // Parse month (YYYY-MM) to date range - const [year, monthNum] = month.split("-").map(Number); - const startDate = new Date(Date.UTC(year, monthNum - 1, 1, 0, 0, 0)); - const endDate = new Date(Date.UTC(year, monthNum, 0, 23, 59, 59, 999)); - - console.log( - `[syncMissingEmpTypeByMonth] Processing ${profileType} for ${month} (${startDate.toISOString()} to ${endDate.toISOString()})`, - ); - - // Initialize rate limiter if rate limiting is enabled - if (rateLimit && rateLimit > 0) { - rateLimiter = new RateLimiter(rateLimit); - console.log( - `[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`, - ); - } - - // Select repository based on profile type - const repo = profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo; - - // Query profiles updated within the month - const profiles = await repo - .createQueryBuilder("p") - .where("p.keycloak IS NOT NULL") - .andWhere("p.keycloak != :empty", { empty: "" }) - .andWhere("p.isDelete = :isDelete", { isDelete: false }) - .andWhere("p.lastUpdatedAt BETWEEN :start AND :end", { - start: startDate, - end: endDate, - }) - .orderBy("p.lastUpdatedAt", "ASC") - .getMany(); - - result.totalProfiles = profiles.length; - console.log(`[syncMissingEmpTypeByMonth] Found ${profiles.length} profiles to check`); - - if (profiles.length === 0) { - result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`; - return result; - } - - // Process profiles in parallel with concurrency limit - for (let i = 0; i < profiles.length; i += concurrency) { - const batch = profiles.slice(i, i + concurrency); - - await Promise.all( - batch.map(async (profile) => { - // Apply rate limiting if enabled - if (rateLimiter) { - await rateLimiter.throttle(); - } - - const keycloakUserId = profile.keycloak; - if (!keycloakUserId) { - return { - profileId: profile.id, - status: "skipped" as const, - reason: "No keycloak ID", - }; - } - - try { - // Check if empType is empty in Keycloak - const { isEmpty, currentEmpType } = await this.checkEmpTypeEmpty(keycloakUserId); - - result.profilesChecked++; - - if (!isEmpty) { - result.skipped++; - return { - profileId: profile.id, - status: "skipped" as const, - reason: "empType already exists", - empType: currentEmpType, - }; - } - - result.missingEmpType++; - - if (dryRun) { - return { - profileId: profile.id, - status: "skipped" as const, - reason: "dry run", - wouldSync: true, - }; - } - - // Sync the profile - const success = await withRetry( - async () => this.syncOnOrganizationChange(profile.id, profileType), - 3, // maxRetries - 1000, // baseDelay - ); - - if (success) { - result.syncSuccess++; - return { - profileId: profile.id, - status: "synced" as const, - }; - } else { - result.syncFailed++; - return { - profileId: profile.id, - status: "failed" as const, - reason: "Sync returned false", - }; - } - } catch (error: any) { - result.syncFailed++; - return { - profileId: profile.id, - status: "failed" as const, - reason: error.message || "Unknown error", - }; - } - }), - ); - - // Log progress every 50 profiles - const completed = Math.min(i + concurrency, profiles.length); - if (completed % 50 === 0 || completed === profiles.length) { - console.log( - `[syncMissingEmpTypeByMonth] Progress: ${completed}/${profiles.length} profiles processed`, - ); - } - } - - result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`; - console.log( - `[syncMissingEmpTypeByMonth] Completed: total=${result.totalProfiles}, checked=${result.profilesChecked}, missing=${result.missingEmpType}, synced=${result.syncSuccess}, failed=${result.syncFailed}, skipped=${result.skipped}, elapsed=${result.executionTime}`, - ); - } catch (error) { - console.error("[syncMissingEmpTypeByMonth] Error:", error); - throw error; - } - - return result; - } - /** * Clear org DNA attributes in Keycloak for given profiles * Sets all org DNA fields to empty strings @@ -768,13 +551,7 @@ export class KeycloakAttributeService { maxRetries?: number; // Retry attempts for failed operations rateLimit?: number; // Requests per second clearProgress?: boolean; // Start fresh, ignore existing progress - }): Promise<{ - total: number; - success: number; - failed: number; - details: any[]; - resumed?: boolean; - }> { + }): Promise<{ total: number; success: number; failed: number; details: any[]; resumed?: boolean }> { const limit = options?.limit; const concurrency = options?.concurrency ?? 5; const resume = options?.resume ?? false; @@ -928,10 +705,7 @@ export class KeycloakAttributeService { // Save progress after each batch SyncProgressManager.save(updatedState); // Log progress every 50 items - if ( - updatedState.lastSyncedIndex % 50 === 0 || - updatedState.lastSyncedIndex === updatedState.totalProfiles - ) { + if (updatedState.lastSyncedIndex % 50 === 0 || updatedState.lastSyncedIndex === updatedState.totalProfiles) { SyncProgressManager.logProgress(updatedState); } }, diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index b6514eca..44916aee 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -1,4 +1,4 @@ -import { EntityManager, In } from "typeorm"; +import { In } from "typeorm"; import { SavePosMasterHistory } from "./../interfaces/OrgMapping"; import { AppDataSource } from "../database/data-source"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; @@ -12,185 +12,110 @@ import { Position } from "../entities/Position"; import { ProfileEducation } from "../entities/ProfileEducation"; import { RequestWithUser } from "../middlewares/user"; -/** - * function สำหรับดึงตำแหน่งที่รักษาการแทน - * ใช้กรณี positionSign ว่าง - * - ถ้า posType = "อำนวยการ" หรือ "บริหาร" ใช้ posExecutiveName - * - ถ้า posType อื่นๆ ใช้ positionName + posLevel - */ -export async function getPosMasterPositions( - posMasterIds: string[] -): Promise> { - if (posMasterIds.length === 0) { - return new Map(); - } - - const positionRepo = AppDataSource.getRepository(Position); - - // Query รอบที่ 1: หา position ที่มีคนครอง - const positionsWithHolder = await positionRepo.find({ - where: { - posMasterId: In(posMasterIds), - positionIsSelected: true, - }, - relations: ["posType", "posLevel", "posExecutive"], - }); - - // หา posMasterId ที่ยังไม่ได้ผลลัพธ์ - const foundMasterIds = new Set(positionsWithHolder.map((p) => p.posMasterId)); - const missingMasterIds = posMasterIds.filter((id) => !foundMasterIds.has(id)); - - // Query รอบที่ 2: เฉพาะที่ขาด (กรณีไม่มีคนครอง) - let positionsWithoutHolder: Position[] = []; - if (missingMasterIds.length > 0) { - positionsWithoutHolder = await positionRepo.find({ - where: { - posMasterId: In(missingMasterIds), - }, - order: { createdAt: "ASC" }, - relations: ["posType", "posLevel", "posExecutive"], - }); - } - - // รวม positions และสร้าง Map - const allPositions = [...positionsWithHolder, ...positionsWithoutHolder]; - const positionMap = new Map(); - - for (const pos of allPositions) { - const posTypeName = pos.posType?.posTypeName || ""; - let positionText = ""; - - if (posTypeName === "อำนวยการ" || posTypeName === "บริหาร") { - positionText = pos.posExecutive?.posExecutiveName || `${pos.positionName || ""}ระดับ${pos.posLevel?.posLevelName || ""}`.trim(); - } else { - positionText = `${pos.positionName || ""}${pos.posLevel?.posLevelName || ""}`.trim(); - } - - positionMap.set(pos.posMasterId, positionText); - } - - return positionMap; -} - - export async function CreatePosMasterHistoryOfficer( posMasterId: string, request: RequestWithUser | null, type?: string | null, positionData?: { positionId?: string } | null, - manager?: EntityManager, ): Promise { - const execute = async (transactionManager: EntityManager) => { - const repoPosmaster = transactionManager.getRepository(PosMaster); - const repoHistory = transactionManager.getRepository(PosMasterHistory); - const repoOrgRevision = transactionManager.getRepository(OrgRevision); - const repoPosition = transactionManager.getRepository(Position); - - const pm = await repoPosmaster.findOne({ - where: { id: posMasterId }, - relations: [ - "positions", - "positions.posLevel", - "positions.posType", - "positions.posExecutive", - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - "next_holder", - ], - }); - - if (!pm || !pm.ancestorDNA) { - return; - } - - const checkCurrentRevision = await repoOrgRevision.findOne({ - where: { - id: pm.orgRevisionId, - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - const _null: any = null; - const h = new PosMasterHistory(); - - // query position โดยตรงจาก positionRepository - let selectedPosition: Position | null = null; - if (positionData?.positionId) { - selectedPosition = await repoPosition.findOne({ - where: { id: positionData.positionId }, - relations: { posLevel: true, posType: true, posExecutive: true }, - }); - } else { - // ใช้ logic เดิม หาจาก pm.positions ที่ positionIsSelected = true - selectedPosition = - pm.positions.length > 0 - ? pm.positions.find((p) => p.positionIsSelected === true) ?? null - : null; - } - - h.ancestorDNA = pm.ancestorDNA ? pm.ancestorDNA : _null; - if (!type || type != "DELETE") { - 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; - } - h.position = selectedPosition?.positionName ?? _null; - h.posType = selectedPosition?.posType?.posTypeName ?? _null; - h.posLevel = selectedPosition?.posLevel?.posLevelName ?? _null; - } - h.rootDnaId = pm.orgRoot?.ancestorDNA || _null; - h.child1DnaId = pm.orgChild1?.ancestorDNA || _null; - h.child2DnaId = pm.orgChild2?.ancestorDNA || _null; - h.child3DnaId = pm.orgChild3?.ancestorDNA || _null; - h.child4DnaId = pm.orgChild4?.ancestorDNA || _null; - h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null; - h.posMasterNo = pm.posMasterNo ?? _null; - h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null; - h.posExecutive = selectedPosition?.posExecutive?.posExecutiveName ?? _null; - h.shortName = - [ - pm.orgChild4?.orgChild4ShortName, - pm.orgChild3?.orgChild3ShortName, - pm.orgChild2?.orgChild2ShortName, - pm.orgChild1?.orgChild1ShortName, - pm.orgRoot?.orgRootShortName, - ].find((s) => typeof s === "string" && s.trim().length > 0) ?? _null; - const userId = request?.user?.sub ?? ""; - const userName = request?.user?.name ?? "system"; - h.createdUserId = userId; - h.createdFullName = userName; - h.lastUpdateUserId = userId; - h.lastUpdateFullName = userName; - h.createdAt = new Date(); - h.lastUpdatedAt = new Date(); - await repoHistory.save(h); - }; - try { - if (manager) { - await execute(manager); - return true; - } + await AppDataSource.transaction(async (manager) => { + const repoPosmaster = manager.getRepository(PosMaster); + const repoHistory = manager.getRepository(PosMasterHistory); + const repoOrgRevision = manager.getRepository(OrgRevision); + const repoPosition = manager.getRepository(Position); - await AppDataSource.transaction(async (transactionManager) => { - await execute(transactionManager); + const pm = await repoPosmaster.findOne({ + where: { id: posMasterId }, + relations: [ + "positions", + "positions.posLevel", + "positions.posType", + "positions.posExecutive", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + "next_holder", + ], + }); + + if (!pm) return false; + if (!pm.ancestorDNA) return false; + + const checkCurrentRevision = await repoOrgRevision.findOne({ + where: { + id: pm.orgRevisionId, + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + const _null: any = null; + const h = new PosMasterHistory(); + + // query position โดยตรงจาก positionRepository + let selectedPosition: Position | null = null; + if (positionData?.positionId) { + selectedPosition = await repoPosition.findOne({ + where: { id: positionData.positionId }, + relations: { posLevel: true, posType: true, posExecutive: true }, + }); + } else { + // ใช้ logic เดิม หาจาก pm.positions ที่ positionIsSelected = true + selectedPosition = + pm.positions.length > 0 + ? pm.positions.find((p) => p.positionIsSelected === true) ?? null + : null; + } + + h.ancestorDNA = pm.ancestorDNA ? pm.ancestorDNA : _null; + if (!type || type != "DELETE") { + 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; + } + h.position = selectedPosition?.positionName ?? _null; + h.posType = selectedPosition?.posType?.posTypeName ?? _null; + h.posLevel = selectedPosition?.posLevel?.posLevelName ?? _null; + } + h.rootDnaId = pm.orgRoot?.ancestorDNA || _null; + h.child1DnaId = pm.orgChild1?.ancestorDNA || _null; + h.child2DnaId = pm.orgChild2?.ancestorDNA || _null; + h.child3DnaId = pm.orgChild3?.ancestorDNA || _null; + h.child4DnaId = pm.orgChild4?.ancestorDNA || _null; + h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null; + h.posMasterNo = pm.posMasterNo ?? _null; + h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null; + h.posExecutive = selectedPosition?.posExecutive?.posExecutiveName ?? _null; + h.shortName = + [ + pm.orgChild4?.orgChild4ShortName, + pm.orgChild3?.orgChild3ShortName, + pm.orgChild2?.orgChild2ShortName, + pm.orgChild1?.orgChild1ShortName, + pm.orgRoot?.orgRootShortName, + ].find((s) => typeof s === "string" && s.trim().length > 0) ?? _null; + const userId = request?.user?.sub ?? ""; + const userName = request?.user?.name ?? "system"; + h.createdUserId = userId; + h.createdFullName = userName; + h.lastUpdateUserId = userId; + h.lastUpdateFullName = userName; + h.createdAt = new Date(); + h.lastUpdatedAt = new Date(); + await repoHistory.save(h); }); + return true; } catch (err) { - if (manager) { - console.error("CreatePosMasterHistoryOfficer error (external transaction):", err); - throw err; - } console.error("CreatePosMasterHistoryOfficer transaction error:", err); return false; } @@ -199,7 +124,6 @@ export async function CreatePosMasterHistoryOfficer( export async function CreatePosMasterHistoryEmployee( posMasterId: string, request: RequestWithUser | null, - type?: string | null, ): Promise { try { await AppDataSource.transaction(async (manager) => { @@ -230,23 +154,15 @@ export async function CreatePosMasterHistoryEmployee( ? pm.positions.find((p) => p.positionIsSelected === true) ?? null : null; h.ancestorDNA = pm.ancestorDNA; - if (!type || type != "DELETE") { - h.profileEmployeeId = pm.current_holder?.id || _null; - h.prefix = pm.current_holder?.prefix || _null; - h.firstName = pm.current_holder?.firstName || _null; - h.lastName = pm.current_holder?.lastName || _null; - h.position = selectedPosition?.positionName ?? _null; - h.posType = selectedPosition?.posType?.posTypeName ?? _null; - h.posLevel = selectedPosition?.posLevel?.posLevelName ?? _null; - } - h.rootDnaId = pm.orgRoot?.ancestorDNA || _null; - h.child1DnaId = pm.orgChild1?.ancestorDNA || _null; - h.child2DnaId = pm.orgChild2?.ancestorDNA || _null; - h.child3DnaId = pm.orgChild3?.ancestorDNA || _null; - h.child4DnaId = pm.orgChild4?.ancestorDNA || _null; + h.prefix = pm.current_holder?.prefix || _null; + h.firstName = pm.current_holder?.firstName || _null; + h.lastName = pm.current_holder?.lastName || _null; h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null; h.posMasterNo = pm.posMasterNo ?? _null; h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null; + h.position = selectedPosition?.positionName ?? _null; + h.posType = selectedPosition?.posType?.posTypeName ?? _null; + h.posLevel = selectedPosition?.posLevel?.posLevelName ?? _null; h.shortName = [ pm.orgChild4?.orgChild4ShortName, diff --git a/src/services/RetirementService.ts b/src/services/RetirementService.ts deleted file mode 100644 index 3e8a1923..00000000 --- a/src/services/RetirementService.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { AppDataSource } from "../database/data-source"; -import { Profile } from "../entities/Profile"; -import { PostRetireToExprofile } from "../controllers/ExRetirementController"; -import { Between, MoreThanOrEqual } from "typeorm"; - -const BATCH_SIZE = 100; -const CONCURRENT_PER_BATCH = 10; // ส่ง parallel ทีละ 10 คนในแต่ละ batch - -export class RetirementService { - private profileRepository = AppDataSource.getRepository(Profile); - - /** - * Cronjob สำหรับส่งข้อมูลผู้เกษียณไปยังระบบพ้นราชการ (Exprofile) - * ทำงานเวลา 04:30:00 ของทุกวันที่ 1 ตุลาคม - * - * รายละเอียด: - * - Query profiles ที่ leaveDate = วันที่ 1 ตุลาคมของปีนั้น และ leaveType = "RETIRE" - * - Batch ทีละ 100 records - * - Concurrent ทีละ 10 คน (parallel) ในแต่ละ batch - * - ถ้า fail ให้ log error แล้วทำคนต่อไป - */ - async cronjobPostRetireToExprofile(): Promise<{ - success: number; - failed: number; - failedProfiles: Array<{ id: string; name: string; error: string }>; - }> { - const result = { - success: 0, - failed: 0, - failedProfiles: [] as Array<{ id: string; name: string; error: string }>, - }; - - try { - // หาวันที่ 1 ตุลาคมของปีปัจจุบัน - const now = new Date(); - const currentYear = now.getFullYear(); - - // สร้างวันที่ 1 ตุลาคมของปีปัจจุบัน (เวลา 00:00:00) - const startDate = new Date(currentYear, 9, 1, 0, 0, 0); // Month 9 = October (0-indexed) - const endDate = new Date(currentYear, 9, 1, 23, 59, 59); - - // Query profiles ที่ leaveDate อยู่ในวันที่ 1 ตุลาคม และ leaveType = "RETIRE" - const profiles = await this.profileRepository.find({ - where: [ - { leaveDate: Between(startDate, endDate), leaveType: "RETIRE" as any }, - { leaveDate: MoreThanOrEqual(startDate), leaveType: "RETIRE" as any }, - ], - relations: ["posLevel", "posType"], - }); - - // Filter เอาเฉพาะวันที่ 1 ตุลาคมเท่านั้น - const filteredProfiles = profiles.filter((p) => { - if (!p.leaveDate) return false; - const leaveDate = new Date(p.leaveDate); - return ( - leaveDate.getFullYear() === currentYear && - leaveDate.getMonth() === 9 && // October - leaveDate.getDate() === 1 - ); - }); - - if (filteredProfiles.length === 0) { - return result; - } - - // แบ่ง batch ทีละ 100 records - for (let i = 0; i < filteredProfiles.length; i += BATCH_SIZE) { - const batch = filteredProfiles.slice(i, i + BATCH_SIZE); - - // แบ่งเป็น chunk เล็กๆ ทีละ CONCURRENT_PER_BATCH เพื่อส่ง parallel - for (let j = 0; j < batch.length; j += CONCURRENT_PER_BATCH) { - const chunk = batch.slice(j, j + CONCURRENT_PER_BATCH); - - // ส่ง parallel ในแต่ละ chunk - await Promise.all( - chunk.map(async (profile) => { - try { - await this.postSingleProfileToExprofile(profile); - result.success++; - } catch (error: any) { - result.failed++; - const errorInfo = { - id: profile.id, - name: `${profile.prefix}${profile.firstName} ${profile.lastName}`, - error: error.message || String(error), - }; - result.failedProfiles.push(errorInfo); - } - }), - ); - } - } - } catch (error: any) { - // Log error but don't throw - allow cronjob to complete with partial results - console.error("[cronjobPostRetireToExprofile] Error:", error); - // Return current results instead of throwing - return result; - } - - return result; - } - - /** - * ส่งข้อมูล profile ไปยัง Exprofile - */ - private async postSingleProfileToExprofile(profile: Profile): Promise { - if (!profile.leaveDate) { - return; - } - - if (!profile.citizenId) { - return; - } - - const retireDate = new Date(profile.leaveDate); - const retireYear = retireDate.getFullYear(); - - // Validate date is valid - if (isNaN(retireYear) || retireYear < 2000) { - throw new Error(`Invalid leaveDate for profile ${profile.id}: ${profile.leaveDate}`); - } - - // ส่งไปยัง Exprofile - await PostRetireToExprofile( - null, - profile.citizenId, - profile.prefix || "", - profile.firstName || "", - profile.lastName || "", - retireYear.toString(), - profile.position || "", - profile.posType?.posTypeName || "", - profile.posLevel?.posLevelName || "", - retireDate, - profile.org || "", - profile.leaveReason || "เกษียณอายุราชการ", - ); - } -} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index c00b8150..a8011900 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -1,11 +1,8 @@ -import { randomUUID } from "crypto"; import amqp from "amqplib"; -import { promisify } from "util"; import { AppDataSource } from "../database/data-source"; import { Command } from "../entities/Command"; import { chunkArray, commandTypePath } from "../interfaces/utils"; import CallAPI from "../interfaces/call-api"; -import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { PosMaster } from "../entities/PosMaster"; @@ -22,35 +19,13 @@ import { OrgChild4 } from "../entities/OrgChild4"; import { OrgRoot } from "../entities/OrgRoot"; import { PosMasterAssign, PosMasterAssignDTO } from "../entities/PosMasterAssign"; import { Position } from "../entities/Position"; -import { In, Not, Repository } from "typeorm"; +import { In, Not } from "typeorm"; import { PosMasterAct } from "../entities/PosMasterAct"; import { PermissionOrg } from "../entities/PermissionOrg"; import { sendWebSocket } from "./webSocket"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; import { PayloadSendNoti } from "../interfaces/utils"; import { PermissionProfile } from "../entities/PermissionProfile"; -import { PosMasterHistory } from "../entities/PosMasterHistory"; - -const redis = require("redis"); -const REDIS_HOST = process.env.REDIS_HOST; -const REDIS_PORT = process.env.REDIS_PORT; - -let reconnectTimer: ReturnType | null = null; - -function scheduleReconnect() { - if (reconnectTimer) { - return; - } - - reconnectTimer = setTimeout(async () => { - reconnectTimer = null; - try { - await init(); - } catch (error) { - console.error("[AMQ] Reconnect failed:", error); - scheduleReconnect(); - } - }, 1000); -} export let sendToQueue: (payload: any) => void; export let sendToQueueOrg: (payload: any) => void; @@ -79,28 +54,10 @@ export async function init() { console.log(connection ? "[AMQ] Connection success" : "[AMQ] Connection failed"); - connection.on("error", (error) => { - console.error("[AMQ] Connection error:", error); - }); - - connection.on("close", () => { - console.error("[AMQ] Connection closed. Scheduling reconnect..."); - scheduleReconnect(); - }); - const channel = await connection.createChannel(); //----> (1.4) create Channel console.log(channel ? "[AMQ] Create channel success" : "[AMQ] Create channel failed"); - channel.on("error", (error) => { - console.error("[AMQ] Channel error:", error); - }); - - channel.on("close", () => { - console.error("[AMQ] Channel closed. Scheduling reconnect..."); - scheduleReconnect(); - }); - channel.assertQueue(queue, { durable: true }), //----> (1.5) assert queue and set durable (if "true" save to disk on RabbitMQ) channel.assertQueue(queue_org, { durable: true }), channel.assertQueue(queue_org_draft, { durable: true }), @@ -134,6 +91,8 @@ export async function init() { // createConsumer(queue2, channel, handler2); } +let retries = 0; + function createConsumer( //----> consumer queue: string, channel: amqp.Channel, @@ -143,24 +102,13 @@ function createConsumer( //----> consumer queue, async (msg) => { if (!msg) return; - try { - if (await handler(msg)) { - console.log("[AMQ] Process Consumer success"); - return channel.ack(msg); - } - console.error( - `[AMQ] Process Consumer failed on queue ${queue}, acknowledging without retry`, - ); + if ((await handler(msg)) || retries++ >= 3) { + retries = 0; + console.log("[AMQ] Process Consumer success"); return channel.ack(msg); - } catch (error) { - console.error(`[AMQ] Consumer processing error on queue ${queue}:`, error); - try { - console.error(`[AMQ] Acknowledging failed message on queue ${queue} without retry`); - channel.ack(msg); - } catch (channelError) { - console.error(`[AMQ] Failed to ack/nack message on queue ${queue}:`, channelError); - } } + console.log("[AMQ] Process Consumer failed"); + return await new Promise((resolve) => setTimeout(() => resolve(channel.nack(msg)), 3000)); }, { noAck: false }, ); @@ -456,7 +404,7 @@ async function handler_command_noti(msg: amqp.ConsumeMessage): Promise try { let profilesNotiRequest: Promise | undefined; - if (!["C-PM-10"].includes(command.commandType.code)) { + if (!(["C-PM-10"].includes(command.commandType.code))) { profilesNotiRequest = new CallAPI() .PostData( { headers: { authorization: token } }, @@ -492,14 +440,14 @@ async function handler_command_noti(msg: amqp.ConsumeMessage): Promise let profilesSend = command && command.commandSends.length > 0 ? command.commandSends - .filter((x: any) => x.profileId != null) - .map((x: any) => ({ - receiverUserId: x.profileId, - notiLink: "", - isSendMail: x.commandSendCCs.map((x: any) => x.name == "EMAIL").length > 0, - isSendInbox: x.commandSendCCs.map((x: any) => x.name == "INBOX").length > 0, - isSendNotification: true, - })) + .filter((x: any) => x.profileId != null) + .map((x: any) => ({ + receiverUserId: x.profileId, + notiLink: "", + isSendMail: x.commandSendCCs.map((x: any) => x.name == "EMAIL").length > 0, + isSendInbox: x.commandSendCCs.map((x: any) => x.name == "INBOX").length > 0, + isSendNotification: true, + })) : []; const payloadStr = await PayloadSendNoti(command.id); const profilesSendRequest = new CallAPI() @@ -533,7 +481,8 @@ async function handler_command_noti(msg: amqp.ConsumeMessage): Promise /*เฉพาะคำสั่ง C-PM-10 ให้ตัด profilesNotiRequest ที่ส่ง noti ครั้งแรกออก*/ if (["C-PM-10"].includes(command.commandType.code)) { await Promise.all([profilesSendRequest]); - } else { + } + else { await Promise.all([profilesNotiRequest!, profilesSendRequest]); } @@ -547,53 +496,23 @@ async function handler_command_noti(msg: amqp.ConsumeMessage): Promise async function handler_org(msg: amqp.ConsumeMessage): Promise { //----> condition before process consume - console.time("[AMQ] handler_org_total"); - const startTime = Date.now(); - console.log(`[AMQ] handler_org START at ${new Date(startTime).toISOString()}`); - const repoPosmaster = AppDataSource.getRepository(PosMaster); const posMasterAssignRepository = AppDataSource.getRepository(PosMasterAssign); const posMasterActRepository = AppDataSource.getRepository(PosMasterAct); - // const permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile); - // const repoEmployeePosmaster = AppDataSource.getRepository(EmployeePosMaster); - // const repoEmployeeTempPosmaster = AppDataSource.getRepository(EmployeeTempPosMaster); + const permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile); + const repoEmployeePosmaster = AppDataSource.getRepository(EmployeePosMaster); + const repoEmployeeTempPosmaster = AppDataSource.getRepository(EmployeeTempPosMaster); const repoProfile = AppDataSource.getRepository(Profile); - // const repoProfileEmployee = AppDataSource.getRepository(ProfileEmployee); - // const employeePositionRepository = AppDataSource.getRepository(EmployeePosition); + const repoProfileEmployee = AppDataSource.getRepository(ProfileEmployee); + const employeePositionRepository = AppDataSource.getRepository(EmployeePosition); const repoOrgRevision = AppDataSource.getRepository(OrgRevision); - // const orgRootRepository = AppDataSource.getRepository(OrgRoot); - // const child1Repository = AppDataSource.getRepository(OrgChild1); - // const child2Repository = AppDataSource.getRepository(OrgChild2); - // const child3Repository = AppDataSource.getRepository(OrgChild3); - // const child4Repository = AppDataSource.getRepository(OrgChild4); - const { data, user } = JSON.parse(msg.content.toString()); + const orgRootRepository = AppDataSource.getRepository(OrgRoot); + const child1Repository = AppDataSource.getRepository(OrgChild1); + const child2Repository = AppDataSource.getRepository(OrgChild2); + const child3Repository = AppDataSource.getRepository(OrgChild3); + const child4Repository = AppDataSource.getRepository(OrgChild4); + const { data, token, user } = JSON.parse(msg.content.toString()); const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data; - console.log(`[AMQ] Received message - revisionId: ${id}, status: ${status}`); - - const targetOrgRevision = await repoOrgRevision.findOne({ - where: { id }, - }); - - if (!targetOrgRevision) { - console.error(`[AMQ] Skip publish: revision ${id} not found`); - console.timeEnd("[AMQ] handler_org_total"); - return true; - } - - if (targetOrgRevision.orgRevisionIsCurrent && !targetOrgRevision.orgRevisionIsDraft) { - console.log(`[AMQ] Skip publish: revision ${id} is already current`); - console.timeEnd("[AMQ] handler_org_total"); - return true; - } - - if (!targetOrgRevision.orgRevisionIsDraft || targetOrgRevision.orgRevisionIsCurrent) { - console.log( - `[AMQ] Skip publish: revision ${id} is no longer publishable (isDraft=${targetOrgRevision.orgRevisionIsDraft}, isCurrent=${targetOrgRevision.orgRevisionIsCurrent})`, - ); - console.timeEnd("[AMQ] handler_org_total"); - return true; - } - if (user) { sendWebSocket( "send-publish-org", @@ -604,75 +523,30 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { { userId: user?.sub }, ).catch(console.error); } + const orgRevisionPublish = await repoOrgRevision + .createQueryBuilder("orgRevision") + .where("orgRevision.orgRevisionIsDraft = false") + .andWhere("orgRevision.orgRevisionIsCurrent = true") + .getOne(); - console.time("[AMQ] query_revisions"); - const [orgRevisionPublish, orgRevisionDraft] = await Promise.all([ - repoOrgRevision - .createQueryBuilder("orgRevision") - .where("orgRevision.orgRevisionIsDraft = false") - .andWhere("orgRevision.orgRevisionIsCurrent = true") - .getOne(), - repoOrgRevision - .createQueryBuilder("orgRevision") - .where("orgRevision.orgRevisionIsDraft = true") - .andWhere("orgRevision.orgRevisionIsCurrent = false") - .getOne(), - ]); - console.timeEnd("[AMQ] query_revisions"); - console.log( - `[AMQ] orgRevisionPublish found: ${orgRevisionPublish ? orgRevisionPublish.id : "null"}`, - ); - console.log(`[AMQ] orgRevisionDraft found: ${orgRevisionDraft ? orgRevisionDraft.id : "null"}`); - - // Validate: ต้องมี orgRevisionPublish เสมอสำหรับการเผยแพร่ - if (!orgRevisionPublish) { - console.error( - "[AMQ] Cannot publish: No current org revision found (isDraft=false, isCurrent=true)", - ); - if (user) { - sendWebSocket( - "send-publish-org", - { - success: false, - message: `ไม่พบข้อมูลโครงสร้างหน่วยงานปัจจุบัน ไม่สามารถเผยแพร่ได้`, - }, - { userId: user?.sub }, - ).catch(console.error); - } - return false; + const orgRevisionDraft = await repoOrgRevision + .createQueryBuilder("orgRevision") + .where("orgRevision.orgRevisionIsDraft = true") + .andWhere("orgRevision.orgRevisionIsCurrent = false") + .getOne(); + if (orgRevisionPublish) { + //เข้าเงื่อนไขจะเปลี่ยนสถานะ orgRevisionPublish เป็นไม่ใช่ current และไม่เป็น daft + orgRevisionPublish.orgRevisionIsDraft = false; + orgRevisionPublish.orgRevisionIsCurrent = false; + await repoOrgRevision.save(orgRevisionPublish); } - - // Validate: ต้องมี orgRevisionDraft ที่จะเผยแพร่ - if (!orgRevisionDraft) { - console.error( - "[AMQ] Cannot publish: No draft org revision found (isDraft=true, isCurrent=false)", - ); - if (user) { - sendWebSocket( - "send-publish-org", - { - success: false, - message: `ไม่พบข้อมูลโครงสร้างหน่วยงานแบบร่าง ไม่สามารถเผยแพร่ได้`, - }, - { userId: user?.sub }, - ).catch(console.error); - } - return false; + if (orgRevisionDraft) { + //เข้าเงื่อนไขจะเปลี่ยนสถานะ orgRevisionDraft เป็นไม่ใช่ daft และเป็น current + orgRevisionDraft.orgRevisionIsCurrent = true; + orgRevisionDraft.orgRevisionIsDraft = false; + await repoOrgRevision.save(orgRevisionDraft); } - - if (orgRevisionDraft.id !== targetOrgRevision.id) { - console.log( - `[AMQ] Skip publish: revision ${id} is stale because draft ${orgRevisionDraft.id} is now the active publish candidate`, - ); - console.timeEnd("[AMQ] handler_org_total"); - return true; - } - - // NOTE: ย้ายการอัปเดตสถานะไปไว้หลังจากทำงานเสร็จทั้งหมด - // เพื่อป้องกันกรณี timeout/retry ทำให้สถานะเพี้ยน (ทุก row เป็น false,false) - try { - console.time("[AMQ] query_posMaster"); const posMaster = await repoPosmaster.find({ where: { orgRevisionId: id }, relations: [ @@ -687,31 +561,23 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { "positions.posExecutive", ], }); - console.timeEnd("[AMQ] query_posMaster"); - console.log(`[AMQ] posMaster count: ${posMaster.length}`); - console.time("[AMQ] query_old_data"); const oldPosMasters = await repoPosmaster.find({ where: { - orgRevisionId: orgRevisionPublish.id, + orgRevisionId: orgRevisionPublish!.id, }, - select: ["id", "current_holderId", "ancestorDNA"], + select: ['id', 'current_holderId', 'ancestorDNA'] }); - // Task #2160 ดึง posMasterAssign ของ revision เดิม + // Task #2160 ดึง posMasterAssign ของ revision เดิม const oldposMasterAssigns = await posMasterAssignRepository.find({ relations: ["posMaster"], where: { posMaster: { - orgRevisionId: orgRevisionPublish.id, + orgRevisionId: orgRevisionPublish!.id, }, }, }); - console.timeEnd("[AMQ] query_old_data"); - console.log(`[AMQ] oldPosMasters count: ${oldPosMasters.length}`); - console.log(`[AMQ] oldposMasterAssigns count: ${oldposMasterAssigns.length}`); - - console.time("[AMQ] build_assignMap"); // สร้าง assignMap เอาไว้เก็บ posMasterAssign.ancestorDNA ของ revision เดิม const assignMap = new Map(); for (const posmasterAssign of oldposMasterAssigns) { @@ -722,31 +588,26 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { assignMap.get(dna)!.push({ id: posmasterAssign.id, posMasterId: posmasterAssign.posMasterId, - assignId: posmasterAssign.assignId, + assignId: posmasterAssign.assignId }); } - console.timeEnd("[AMQ] build_assignMap"); - console.time("[AMQ] query_oldposMasterAct"); // ดึง posMasterAct ของ revision เดิม xxx const oldposMasterAct = await posMasterActRepository.find({ relations: ["posMaster", "posMasterChild"], where: { posMaster: { - orgRevisionId: orgRevisionPublish.id, + orgRevisionId: orgRevisionPublish!.id, }, }, }); - console.timeEnd("[AMQ] query_oldposMasterAct"); - console.log(`[AMQ] oldposMasterAct count: ${oldposMasterAct.length}`); type ActKey = string; // `${parentDNA}|${childDNA}` - console.time("[AMQ] build_maps"); const posMasterActMap = new Map(); for (const act of oldposMasterAct) { - const parentDNA = act.posMaster?.ancestorDNA?.trim() ?? ""; - const childDNA = act.posMasterChild?.ancestorDNA?.trim() ?? ""; + const parentDNA = act.posMaster?.ancestorDNA?.trim() ?? ''; + const childDNA = act.posMasterChild?.ancestorDNA?.trim() ?? ''; const key = `${parentDNA}|${childDNA}`; if (!posMasterActMap.has(key)) { @@ -757,7 +618,7 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { const posMasterIdMap = new Map(); for (const pm of posMaster) { - posMasterIdMap.set(pm.ancestorDNA?.trim() ?? "", pm.id); + posMasterIdMap.set(pm.ancestorDNA?.trim() ?? '', pm.id); } const oldPosMasterMap = new Map(); @@ -767,387 +628,146 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { oldPosMasterMap.set(dna, oldPm); } } - console.timeEnd("[AMQ] build_maps"); const _null: any = null; - - // ===== BATCH PROCESSING: เตรียมข้อมูลก่อน loop ===== - console.time("[AMQ] prepare_batch_data"); - // 1. รวบรวม profileIds ทั้งหมดที่ต้องอัพเดท - const profileIds = posMaster - .filter((item) => item.next_holderId != null) - .map((item) => item.next_holderId!) - .filter((id) => id != null && id !== ""); - - // 2. Batch load profiles ทั้งหมดในครั้งเดียว (แก้ปัญหา N+1 Query) - const profilesMap = new Map(); - if (profileIds.length > 0) { - const profiles = await repoProfile.findBy({ - id: In(profileIds), - }); - profiles.forEach((p) => profilesMap.set(p.id, p)); - } - console.log(`[AMQ] profiles to update: ${profilesMap.size}`); - - // 3. เตรียม arrays สำหรับ batch operations - const profilesToSave: Profile[] = []; - const posMasterAssignsToSave: PosMasterAssign[] = []; - const historyRowsToSave: Partial[] = []; - const posMasterUpdates: { id: string; current_holderId: string | null | undefined }[] = []; - - // ===== LOOP: เก็บข้อมูลทั้งหมด ===== for (const item of posMaster) { + const dna = item.ancestorDNA?.trim(); const oldPm = dna ? oldPosMasterMap.get(dna) : null; - // Task #2160 Clone posMasterAssign + // Task #2160 Clone posMasterAssign const assigns = assignMap.get(item.ancestorDNA); if (assigns && assigns.length > 0) { - const newAssigns = assigns.map(({ id, ...fields }) => - posMasterAssignRepository.create({ - ...fields, - posMasterId: item.id, - createdAt: lastUpdatedAt, - createdFullName: lastUpdateFullName, - createdUserId: lastUpdateUserId, - lastUpdatedAt: lastUpdatedAt, - lastUpdateFullName: lastUpdateFullName, - lastUpdateUserId: lastUpdateUserId, - }), - ); - posMasterAssignsToSave.push(...newAssigns); + const newAssigns = assigns.map(({ id, ...fields }) => ({ + ...fields, // copy ทุก field ยกเว้น id + posMasterId: item.id, // ผูกกับ posMasterId ใหม่ + createdAt: lastUpdatedAt, + createdFullName: lastUpdateFullName, + createdUserId: lastUpdateUserId, + lastUpdatedAt: lastUpdatedAt, + lastUpdateFullName: lastUpdateFullName, + lastUpdateUserId: lastUpdateUserId, + })); + await posMasterAssignRepository.save(newAssigns); } - // เตรียมข้อมูลสำหรับ update profile - if (item.next_holderId != null && item.next_holderId !== "") { - const profile = profilesMap.get(item.next_holderId); - if (profile) { - profile.posMasterNo = getPosMasterNo(item) ?? _null; - profile.org = getOrgFullName(item) ?? _null; - - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (!item.isSit && item.positions.length > 0) { - let position = item.positions.find((x) => x.positionIsSelected == true); + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (item.next_holderId != null && !item.isSit) { + const profile = await repoProfile.findOne({ + where: { id: item.next_holderId == null ? "" : item.next_holderId }, + }); + if (profile != null && item.positions.length > 0) { + let position = await item.positions.find((x) => x.positionIsSelected == true); + if (position == null) { + position = await item.positions.find((x) => x.posLevelId == profile?.posLevelId); if (position == null) { - position = item.positions.find((x) => x.posLevelId == profile?.posLevelId); - if (position == null) { - const sorted = [...item.positions].sort((a, b) => a.orderNo - b.orderNo); - position = sorted[0]; - } + position = await item.positions.sort((a, b) => a.orderNo - b.orderNo)[0]; } - - profile.posLevelId = position?.posLevelId ?? _null; - profile.posTypeId = position?.posTypeId ?? _null; - profile.position = position?.positionName ?? _null; - profile.positionField = position?.positionField ?? _null; - profile.posExecutive = position?.posExecutive?.posExecutiveName ?? _null; - profile.positionArea = position?.positionArea ?? _null; - profile.positionExecutiveField = position?.positionExecutiveField ?? _null; } - profilesToSave.push(profile); + profile.posLevelId = position?.posLevelId ?? _null; + profile.posTypeId = position?.posTypeId ?? _null; + profile.position = position?.positionName ?? _null; + await repoProfile.save(profile); } } - - // เก็บข้อมูลสำหรับ update posMaster - posMasterUpdates.push({ - id: item.id, + // item.current_holderId = item.next_holderId; + // item.next_holderId = null; + // item.lastUpdateUserId = lastUpdateUserId; + // item.lastUpdateFullName = lastUpdateFullName; + // item.lastUpdatedAt = lastUpdatedAt; + await repoPosmaster.update(item.id, { current_holderId: item.next_holderId, + next_holderId: null, + lastUpdateUserId, + lastUpdateFullName, + lastUpdatedAt, }); - // เก็บ IDs ที่ต้องสร้าง history const oldHolderId = oldPm ? oldPm.current_holderId : null; - const newHolderId = item?.next_holderId; + const newHolderId = item ? item.next_holderId : null; const isHolderChanged = oldHolderId !== newHolderId; if (isHolderChanged) { - const nextHolderProfile = - item.next_holderId != null && item.next_holderId !== "" - ? profilesMap.get(item.next_holderId) - : null; - const selectedPosition = - item.positions.length > 0 - ? item.positions.find((position) => position.positionIsSelected === true) ?? null - : null; - const shortName = - [ - item.orgChild4?.orgChild4ShortName, - item.orgChild3?.orgChild3ShortName, - item.orgChild2?.orgChild2ShortName, - item.orgChild1?.orgChild1ShortName, - item.orgRoot?.orgRootShortName, - ].find((name) => typeof name === "string" && name.trim().length > 0) ?? _null; - - historyRowsToSave.push({ - ancestorDNA: item.ancestorDNA ? item.ancestorDNA : _null, - prefix: nextHolderProfile?.prefix || _null, - firstName: nextHolderProfile?.firstName || _null, - lastName: nextHolderProfile?.lastName || _null, - shortName, - posMasterNoPrefix: item.posMasterNoPrefix ?? _null, - posMasterNo: item.posMasterNo ?? _null, - posMasterNoSuffix: item.posMasterNoSuffix ?? _null, - position: selectedPosition?.positionName ?? _null, - posType: selectedPosition?.posType?.posTypeName ?? _null, - posLevel: selectedPosition?.posLevel?.posLevelName ?? _null, - posExecutive: selectedPosition?.posExecutive?.posExecutiveName ?? _null, - profileId: _null, - rootDnaId: item.orgRoot?.ancestorDNA || _null, - child1DnaId: item.orgChild1?.ancestorDNA || _null, - child2DnaId: item.orgChild2?.ancestorDNA || _null, - child3DnaId: item.orgChild3?.ancestorDNA || _null, - child4DnaId: item.orgChild4?.ancestorDNA || _null, - createdUserId: "", - createdFullName: "system", - lastUpdateUserId: "", - lastUpdateFullName: "system", - createdAt: new Date(), - lastUpdatedAt: new Date(), - }); + await CreatePosMasterHistoryOfficer(item.id, null); } } - console.timeEnd("[AMQ] prepare_batch_data"); - console.log( - `[AMQ] Prepared - posMasterAssignsToSave: ${posMasterAssignsToSave.length}, profilesToSave: ${profilesToSave.length}, posMasterUpdates: ${posMasterUpdates.length}, historyCreateIds: ${historyRowsToSave.length}`, - ); - // ===== BATCH EXECUTION: save ทีละ batch ===== - let shouldSkipPublishInTransaction = false; - await AppDataSource.transaction(async (manager) => { - const repoPosmaster = manager.getRepository(PosMaster); - const posMasterAssignRepository = manager.getRepository(PosMasterAssign); - const posMasterActRepository = manager.getRepository(PosMasterAct); - const permissionProfilesRepository = manager.getRepository(PermissionProfile); - const repoEmployeePosmaster = manager.getRepository(EmployeePosMaster); - const repoEmployeeTempPosmaster = manager.getRepository(EmployeeTempPosMaster); - const repoProfile = manager.getRepository(Profile); - const repoProfileEmployee = manager.getRepository(ProfileEmployee); - const employeePositionRepository = manager.getRepository(EmployeePosition); - const repoOrgRevision = manager.getRepository(OrgRevision); - const orgRootRepository = manager.getRepository(OrgRoot); - const child1Repository = manager.getRepository(OrgChild1); - const child2Repository = manager.getRepository(OrgChild2); - const child3Repository = manager.getRepository(OrgChild3); - const child4Repository = manager.getRepository(OrgChild4); - const posMasterHistoryRepository = manager.getRepository(PosMasterHistory); + for (const act of oldposMasterAct) { + const parentDNA = act.posMaster?.ancestorDNA?.trim()?.toLowerCase() ?? ''; + const childDNA = act.posMasterChild?.ancestorDNA?.trim()?.toLowerCase() ?? ''; - const targetOrgRevision = await repoOrgRevision - .createQueryBuilder("orgRevision") - .setLock("pessimistic_write") - .where("orgRevision.id = :id", { id }) - .getOne(); + const newParentId = posMasterIdMap.get(parentDNA); + const newChildId = posMasterIdMap.get(childDNA); - if (!targetOrgRevision) { - shouldSkipPublishInTransaction = true; - return; - } + if (!newParentId || !newChildId) continue; - if (targetOrgRevision.orgRevisionIsCurrent && !targetOrgRevision.orgRevisionIsDraft) { - shouldSkipPublishInTransaction = true; - return; - } + const { id, posMaster, posMasterChild, ...fields } = act; - if (!targetOrgRevision.orgRevisionIsDraft || targetOrgRevision.orgRevisionIsCurrent) { - shouldSkipPublishInTransaction = true; - return; - } + const newAct = { + ...fields, + posMasterId: newParentId, + posMasterChildId: newChildId, + createdAt: new Date(), + createdFullName: user ? user.name : "system", + createdUserId: user ? user.sub : "system", + lastUpdatedAt: new Date(), + lastUpdateFullName: user ? user.name : "system", + lastUpdateUserId: user ? user.sub : "system", + }; - const orgRevisionPublish = await repoOrgRevision - .createQueryBuilder("orgRevision") - .setLock("pessimistic_write") - .where("orgRevision.orgRevisionIsDraft = false") - .andWhere("orgRevision.orgRevisionIsCurrent = true") - .getOne(); + await posMasterActRepository.save(newAct); + } - if (!orgRevisionPublish) { - throw new Error("[AMQ] Cannot publish in transaction: no current org revision found"); - } - - const orgRevisionDraft = await repoOrgRevision - .createQueryBuilder("orgRevision") - .setLock("pessimistic_write") - .where("orgRevision.id = :id", { id }) - .andWhere("orgRevision.orgRevisionIsDraft = true") - .andWhere("orgRevision.orgRevisionIsCurrent = false") - .getOne(); - - if (!orgRevisionDraft) { - shouldSkipPublishInTransaction = true; - return; - } - - // 4. Batch save posMasterAssign (chunk 500) - console.time("[AMQ] batch_save_posMasterAssign"); - if (posMasterAssignsToSave.length > 0) { - const chunks = chunkArray(posMasterAssignsToSave, 500); - for (const chunk of chunks) { - await posMasterAssignRepository.save(chunk); - } - } - console.timeEnd("[AMQ] batch_save_posMasterAssign"); - - // 5. Batch save profiles (chunk 200) - console.time("[AMQ] batch_save_profiles"); - if (profilesToSave.length > 0) { - const chunks = chunkArray(profilesToSave, 200); - for (const chunk of chunks) { - await repoProfile.save(chunk); - } - } - console.timeEnd("[AMQ] batch_save_profiles"); - - // 6. Batch update posMasters - console.time("[AMQ] batch_update_posMasters"); - if (posMasterUpdates.length > 0) { - const chunks = chunkArray(posMasterUpdates, 500); - const posMasterTableName = repoPosmaster.metadata.tableName; - for (const chunk of chunks as (typeof posMasterUpdates)[]) { - const caseClauses = chunk.map(() => "WHEN ? THEN ?").join(" "); - const wherePlaceholders = chunk.map(() => "?").join(", "); - const params = chunk.flatMap((update: (typeof posMasterUpdates)[number]) => [ - update.id, - update.current_holderId ?? null, - ]); - - params.push( - lastUpdateUserId, - lastUpdateFullName, - lastUpdatedAt, - ...chunk.map((update: (typeof posMasterUpdates)[number]) => update.id), - ); - - await manager.query( - `UPDATE \`${posMasterTableName}\` - SET current_holderId = CASE id ${caseClauses} END, - next_holderId = NULL, - lastUpdateUserId = ?, - lastUpdateFullName = ?, - lastUpdatedAt = ? - WHERE id IN (${wherePlaceholders})`, - params, - ); - } - } - console.timeEnd("[AMQ] batch_update_posMasters"); - - // 7. Batch create history - console.time("[AMQ] batch_create_history"); - if (historyRowsToSave.length > 0) { - const chunks = chunkArray(historyRowsToSave, 500); - for (const chunk of chunks) { - await posMasterHistoryRepository.save(posMasterHistoryRepository.create(chunk)); - } - } - console.timeEnd("[AMQ] batch_create_history"); - - // Clone oldposMasterAct - console.time("[AMQ] clone_oldposMasterAct"); - const posMasterActRowsToInsert: Partial[] = []; - for (const act of oldposMasterAct) { - const parentDNA = act.posMaster?.ancestorDNA?.trim()?.toLowerCase() ?? ""; - const childDNA = act.posMasterChild?.ancestorDNA?.trim()?.toLowerCase() ?? ""; - - const newParentId = posMasterIdMap.get(parentDNA); - const newChildId = posMasterIdMap.get(childDNA); - - if (!newParentId || !newChildId) continue; - - const { id, posMaster, posMasterChild, ...fields } = act; - - posMasterActRowsToInsert.push({ - ...fields, - posMasterId: newParentId, - posMasterChildId: newChildId, - createdAt: new Date(), - createdFullName: user ? user.name : "system", - createdUserId: user ? user.sub : "system", - lastUpdatedAt: new Date(), - lastUpdateFullName: user ? user.name : "system", - lastUpdateUserId: user ? user.sub : "system", - }); - } - if (posMasterActRowsToInsert.length > 0) { - const chunks = chunkArray(posMasterActRowsToInsert, 500); - for (const chunk of chunks) { - await posMasterActRepository.insert(chunk); - } - } - console.timeEnd("[AMQ] clone_oldposMasterAct"); - - console.time("[AMQ] clone_org_structure"); + if (orgRevisionPublish != null && orgRevisionDraft != null) { //new main revision const before = null; //ทุก orgRoot และ orgChild ข้างล่างนี้จะเป็นตัวเก่าที่ไม่ได้เป็น current revision //cone tree - console.time("[AMQ] query_old_org_structure"); + // if ( + // orgRevisionPublish.typeDraft.toUpperCase() == "ORG" || + // orgRevisionPublish.typeDraft.toUpperCase() == "ORG_POSITION" || + // orgRevisionPublish.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // orgRevisionPublish.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // orgRevisionPublish.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { //หา dna tree - const [orgRoot, orgChild1, orgChild2, orgChild3, orgChild4] = await Promise.all([ - orgRootRepository.find({ - where: { orgRevisionId: orgRevisionPublish.id }, - }), - child1Repository.find({ - where: { orgRevisionId: orgRevisionPublish.id }, - }), - child2Repository.find({ - where: { orgRevisionId: orgRevisionPublish.id }, - }), - child3Repository.find({ - where: { orgRevisionId: orgRevisionPublish.id }, - }), - child4Repository.find({ - where: { orgRevisionId: orgRevisionPublish.id }, - }), - ]); - console.timeEnd("[AMQ] query_old_org_structure"); - console.log( - `[AMQ] Old structure - orgRoot: ${orgRoot.length}, orgChild1: ${orgChild1.length}, orgChild2: ${orgChild2.length}, orgChild3: ${orgChild3.length}, orgChild4: ${orgChild4.length}`, - ); + const orgRoot = await orgRootRepository.find({ + where: { orgRevisionId: orgRevisionPublish.id }, + }); + const orgChild1 = await child1Repository.find({ + where: { orgRevisionId: orgRevisionPublish.id }, + }); + + const orgChild2 = await child2Repository.find({ + where: { orgRevisionId: orgRevisionPublish.id }, + }); + + const orgChild3 = await child3Repository.find({ + where: { orgRevisionId: orgRevisionPublish.id }, + }); + + const orgChild4 = await child4Repository.find({ + where: { orgRevisionId: orgRevisionPublish.id }, + }); // Task #2172 ดึง orgRoot ของ revision ใหม่ const newRoots = await orgRootRepository.find({ where: { orgRevisionId: orgRevisionDraft.id }, }); // สร้าง newRootMap เอาไว้เก็บ orgRoot.ancestorDNA ของ revision ใหม่ - const newRootMap = new Map(newRoots.map((r) => [r.ancestorDNA, r.id])); - const emptyAncestorDNA = "00000000-0000-0000-0000-000000000000"; - const hasEmptyAncestorDNA = (ancestorDNA?: string | null) => - ancestorDNA == null || ancestorDNA === emptyAncestorDNA; - const hasSelfOrEmptyAncestorDNA = (node: { id: string; ancestorDNA: string | null }) => - node.ancestorDNA === node.id || hasEmptyAncestorDNA(node.ancestorDNA); - const findMatchedNodeByAncestorDNA = ( - nodes: T[], - node: T, - ) => - nodes.find((item) => { - if (hasSelfOrEmptyAncestorDNA(node)) { - return hasEmptyAncestorDNA(item.ancestorDNA); - } - return item.ancestorDNA === node.ancestorDNA; - }); - const [ - orgRootCurrent, - orgChild1Current, - orgChild2Current, - orgChild3Current, - orgChild4Current, - ] = await Promise.all([ - orgRootRepository.find({ where: { orgRevisionId: orgRevisionDraft.id } }), - child1Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }), - child2Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }), - child3Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }), - child4Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }), - ]); - - console.time("[AMQ] clone_permissionProfiles"); + const newRootMap = new Map( + newRoots.map(r => [r.ancestorDNA, r.id]) + ); // ดึง permissionProfiles ของ revision เดิม const oldPermissionProfiles = await permissionProfilesRepository.find({ relations: ["orgRootTree"], where: { orgRootTree: { orgRevisionId: orgRevisionPublish.id, - }, - }, + } + } }); const inserts: any[] = []; for (const permiss of oldPermissionProfiles) { @@ -1172,40 +792,33 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { if (inserts.length > 0) { await permissionProfilesRepository.insert(inserts); } - console.timeEnd("[AMQ] clone_permissionProfiles"); - console.time("[AMQ] query_employee_org_structures"); - const [orgemployeePosMaster, orgemployeeTempPosMaster] = await Promise.all([ - repoEmployeePosmaster.find({ - where: { orgRevisionId: orgRevisionPublish.id }, - relations: ["positions"], - }), - repoEmployeeTempPosmaster.find({ - where: { orgRevisionId: orgRevisionPublish.id }, - relations: ["positions"], - }), - ]); - console.timeEnd("[AMQ] query_employee_org_structures"); - console.log(`[AMQ] orgemployeePosMaster count: ${orgemployeePosMaster.length}`); - console.log(`[AMQ] orgemployeeTempPosMaster count: ${orgemployeeTempPosMaster.length}`); + //หา dna posmaster ถ้าไม่มีให้เอาตัวเองเป็น dna + const orgemployeePosMaster = await repoEmployeePosmaster.find({ + where: { orgRevisionId: orgRevisionPublish.id }, + relations: ["positions"], + }); - const currentHolderIds = Array.from( - new Set( - orgemployeePosMaster - .map((item) => item.current_holderId) - .filter((holderId): holderId is string => !!holderId), - ), + let _orgemployeePosMaster: EmployeePosMaster[]; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // _orgemployeePosMaster = orgemployeePosMaster.map((x) => ({ + // ...x, + // ancestorDNA: + // x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000" + // ? x.id + // : x.ancestorDNA, + // })); + // await repoEmployeePosmaster.save(_orgemployeePosMaster); + const validProfileIds = new Set( + (await repoProfileEmployee.find({ select: ["id"] })).map((p) => p.id), ); - const validProfileIds = new Set(); - if (currentHolderIds.length > 0) { - const profiles = await repoProfileEmployee.find({ - select: ["id"], - where: { id: In(currentHolderIds) }, - }); - profiles.forEach((profile) => validProfileIds.add(profile.id)); - } - const _orgemployeePosMaster: EmployeePosMaster[] = orgemployeePosMaster.map((x) => ({ + _orgemployeePosMaster = orgemployeePosMaster.map((x) => ({ ...x, current_holderId: x.current_holderId && validProfileIds.has(x.current_holderId) ? x.current_holderId : null, @@ -1214,17 +827,6 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { ? x.id : x.ancestorDNA, })); - const _orgemployeeTempPosMaster: EmployeeTempPosMaster[] = orgemployeeTempPosMaster.map( - (x) => ({ - ...x, - ancestorDNA: - x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000" - ? x.id - : x.ancestorDNA, - }), - ); - - console.time("[AMQ] insert_employeePosMaster"); await repoEmployeePosmaster .createQueryBuilder() .insert() @@ -1235,173 +837,29 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { overwrite: ["ancestorDNA"], }) .execute(); - console.timeEnd("[AMQ] insert_employeePosMaster"); - const groupByParentId = ( - items: T[], - getParentId: (item: T) => string | null | undefined, - ) => { - const grouped = new Map(); - for (const item of items) { - const parentId = getParentId(item); - if (!parentId) { - continue; - } - const current = grouped.get(parentId); - if (current) { - current.push(item); - } else { - grouped.set(parentId, [item]); - } - } - return grouped; - }; - - const buildEmployeeNodeKey = (item: { - orgRootId?: string | null; - orgChild1Id?: string | null; - orgChild2Id?: string | null; - orgChild3Id?: string | null; - orgChild4Id?: string | null; - }) => { - if (item.orgChild4Id) return `child4:${item.orgChild4Id}`; - if (item.orgChild3Id && item.orgChild4Id == null) return `child3:${item.orgChild3Id}`; - if (item.orgChild2Id && item.orgChild3Id == null) return `child2:${item.orgChild2Id}`; - if (item.orgChild1Id && item.orgChild2Id == null) return `child1:${item.orgChild1Id}`; - if (item.orgRootId && item.orgChild1Id == null) return `root:${item.orgRootId}`; - return null; - }; - - const groupByEmployeeNode = < - T extends { - orgRootId?: string | null; - orgChild1Id?: string | null; - orgChild2Id?: string | null; - orgChild3Id?: string | null; - orgChild4Id?: string | null; - }, - >( - items: T[], - ) => { - const grouped = new Map(); - for (const item of items) { - const key = buildEmployeeNodeKey(item); - if (!key) { - continue; - } - const current = grouped.get(key); - if (current) { - current.push(item); - } else { - grouped.set(key, [item]); - } - } - return grouped; - }; - - const employeePosMasterByNode = groupByEmployeeNode(_orgemployeePosMaster); - const employeeTempPosMasterByNode = groupByEmployeeNode(_orgemployeeTempPosMaster); - const getNodeKey = (level: "root" | "child1" | "child2" | "child3" | "child4", id: string) => - `${level}:${id}`; - const orgChild1ByRoot = groupByParentId(orgChild1, (item) => item.orgRootId); - const orgChild2ByChild1 = groupByParentId(orgChild2, (item) => item.orgChild1Id); - const orgChild3ByChild2 = groupByParentId(orgChild3, (item) => item.orgChild2Id); - const orgChild4ByChild3 = groupByParentId(orgChild4, (item) => item.orgChild3Id); - type OrgDestinationIds = { - orgRootId?: string | null; - orgChild1Id?: string | null; - orgChild2Id?: string | null; - orgChild3Id?: string | null; - orgChild4Id?: string | null; - }; - type CloneEmployeeSource = { - positions: EmployeePosition[]; - } & (EmployeePosMaster | EmployeeTempPosMaster); - const buildAuditFields = (timestamp: Date) => ({ - createdUserId: "", - createdFullName: "System Administrator", - createdAt: timestamp, - lastUpdateUserId: "", - lastUpdateFullName: "System Administrator", - lastUpdatedAt: timestamp, + // } + //หา dna posmaster ถ้าไม่มีให้เอาตัวเองเป็น dna + const orgemployeeTempPosMaster = await repoEmployeeTempPosmaster.find({ + where: { orgRevisionId: orgRevisionPublish.id }, + relations: ["positions"], }); - const buildColumnData = ( - repository: Repository, - source: T, - ): Partial => { - const row = {} as Partial; - const target = row as Record; - const sourceRecord = source as Record; - for (const column of repository.metadata.columns) { - target[column.propertyName] = sourceRecord[column.propertyName]; - } - return row; - }; - const insertInChunks = async ( - repository: Repository, - rows: Partial[], - chunkSize: number, - ) => { - if (rows.length === 0) { - return; - } - for (const chunk of chunkArray(rows, chunkSize) as Partial[][]) { - await repository.insert(chunk as Parameters["insert"]>[0]); - } - }; - const buildEmployeeCloneBatch = ( - items: T[], - parentRepository: Repository, - positionParentKey: "posMasterId" | "posMasterTempId", - targetIds: OrgDestinationIds, - ) => { - const parentRows: Partial[] = []; - const positionRows: Partial[] = []; - - for (const item of items) { - const parentId = randomUUID(); - const parentTimestamp = new Date(); - parentRows.push({ - ...buildColumnData(parentRepository, item), - id: parentId, - orgRevisionId: orgRevisionDraft.id, - ...targetIds, - ...buildAuditFields(parentTimestamp), - }); - - for (const position of item.positions ?? []) { - const positionTimestamp = new Date(); - positionRows.push({ - ...buildColumnData(employeePositionRepository, position), - id: randomUUID(), - posMasterId: positionParentKey === "posMasterId" ? parentId : undefined, - posMasterTempId: positionParentKey === "posMasterTempId" ? parentId : undefined, - ...buildAuditFields(positionTimestamp), - }); - } - } - - return { parentRows, positionRows }; - }; - const cloneEmployeeNodeBatch = async ( - items: T[], - parentRepository: Repository, - positionParentKey: "posMasterId" | "posMasterTempId", - targetIds: OrgDestinationIds, - ) => { - if (items.length === 0) { - return; - } - const { parentRows, positionRows } = buildEmployeeCloneBatch( - items, - parentRepository, - positionParentKey, - targetIds, - ); - await insertInChunks(parentRepository, parentRows, 200); - await insertInChunks(employeePositionRepository, positionRows, 500); - }; + let _orgemployeeTempPosMaster: EmployeeTempPosMaster[]; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + _orgemployeeTempPosMaster = orgemployeeTempPosMaster.map((x) => ({ + ...x, + ancestorDNA: + x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000" + ? x.id + : x.ancestorDNA, + })); + // await repoEmployeeTempPosmaster.save(_orgemployeeTempPosMaster); await repoEmployeeTempPosmaster .createQueryBuilder() .insert() @@ -1412,256 +870,866 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { overwrite: ["ancestorDNA"], }) .execute(); + // } //create org - console.time("[AMQ] forEach_orgRoot"); - console.log(`[AMQ] Starting forEach orgRoot loop (${orgRoot.length} items)`); - for (const x of orgRoot) { - const dataId = x.id; - const matchedOrgRoot = findMatchedNodeByAncestorDNA(orgRootCurrent, x); + orgRoot.forEach(async (x: any) => { + var dataId = x.id; - const filteredEmployeePosMaster = - employeePosMasterByNode.get(getNodeKey("root", dataId)) ?? []; + const orgRootCurrent = await orgRootRepository.find({ + where: { orgRevisionId: orgRevisionDraft.id }, + }); - await cloneEmployeeNodeBatch( - filteredEmployeePosMaster, - repoEmployeePosmaster, - "posMasterId", - { orgRootId: matchedOrgRoot?.id ?? null }, - ); - - await cloneEmployeeNodeBatch( - employeeTempPosMasterByNode.get(getNodeKey("root", dataId)) ?? [], - repoEmployeeTempPosmaster, - "posMasterTempId", - { orgRootId: matchedOrgRoot?.id ?? null }, - ); - - for (const x of orgChild1ByRoot.get(dataId) ?? []) { - const data1Id = x.id; - const matchedOrgChild1 = findMatchedNodeByAncestorDNA(orgChild1Current, x); - await cloneEmployeeNodeBatch( - employeePosMasterByNode.get(getNodeKey("child1", data1Id)) ?? [], - repoEmployeePosmaster, - "posMasterId", - { - orgRootId: matchedOrgRoot?.id ?? null, - orgChild1Id: matchedOrgChild1?.id ?? null, - }, - ); - - await cloneEmployeeNodeBatch( - employeeTempPosMasterByNode.get(getNodeKey("child1", data1Id)) ?? [], - repoEmployeeTempPosmaster, - "posMasterTempId", - { - orgRootId: matchedOrgRoot?.id ?? null, - orgChild1Id: matchedOrgChild1?.id ?? null, - }, - ); - - for (const x of orgChild2ByChild1.get(data1Id) ?? []) { - const data2Id = x.id; - const matchedOrgChild2 = findMatchedNodeByAncestorDNA(orgChild2Current, x); - await cloneEmployeeNodeBatch( - employeePosMasterByNode.get(getNodeKey("child2", data2Id)) ?? [], - repoEmployeePosmaster, - "posMasterId", - { - orgRootId: matchedOrgRoot?.id ?? null, - orgChild1Id: matchedOrgChild1?.id ?? null, - orgChild2Id: matchedOrgChild2?.id ?? null, - }, + const matchedOrgRoot = orgRootCurrent.find((i: OrgRoot) => { + if ( + x.ancestorDNA === x.id || // ถ้า ancestorDNA ถูกตั้งเป็น id ตัวเอง + x.ancestorDNA === null || + x.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ) { + return ( + i.ancestorDNA === null || i.ancestorDNA === "00000000-0000-0000-0000-000000000000" ); + } + return i.ancestorDNA === x.ancestorDNA; + }); - await cloneEmployeeNodeBatch( - employeeTempPosMasterByNode.get(getNodeKey("child2", data2Id)) ?? [], - repoEmployeeTempPosmaster, - "posMasterTempId", - { - orgRootId: dataId, - orgChild1Id: data1Id, - orgChild2Id: data2Id, - }, - ); + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + //create employeePosmaster + await Promise.all( + _orgemployeePosMaster + .filter((x: EmployeePosMaster) => x.orgRootId == dataId && x.orgChild1Id == null) + .map(async (item: any) => { + delete item.id; + const employeePosMaster = Object.assign(new EmployeePosMaster(), item); + employeePosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.current_holderId = item.current_holderId; + // } else { + // // employeePosMaster.next_holderId = null; + // employeePosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.authRoleId = item.authRoleId; + // } else { + // employeePosMaster.authRoleId = null; + // } + // employeePosMaster.current_holderId = null; + employeePosMaster.orgRevisionId = orgRevisionDraft.id; + employeePosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeePosMaster.createdUserId = ""; + employeePosMaster.createdFullName = "System Administrator"; + employeePosMaster.createdAt = new Date(); + employeePosMaster.lastUpdateUserId = ""; + employeePosMaster.lastUpdateFullName = "System Administrator"; + employeePosMaster.lastUpdatedAt = new Date(); + await repoEmployeePosmaster.save(employeePosMaster); - for (const x of orgChild3ByChild2.get(data2Id) ?? []) { - const data3Id = x.id; - const matchedOrgChild3 = findMatchedNodeByAncestorDNA(orgChild3Current, x); - await cloneEmployeeNodeBatch( - employeePosMasterByNode.get(getNodeKey("child3", data3Id)) ?? [], - repoEmployeePosmaster, - "posMasterId", - { - orgRootId: matchedOrgRoot?.id ?? null, - orgChild1Id: matchedOrgChild1?.id ?? null, - orgChild2Id: matchedOrgChild2?.id ?? null, - orgChild3Id: matchedOrgChild3?.id ?? null, - }, - ); - - await cloneEmployeeNodeBatch( - employeeTempPosMasterByNode.get(getNodeKey("child3", data3Id)) ?? [], - repoEmployeeTempPosmaster, - "posMasterTempId", - { - orgRootId: matchedOrgRoot?.id ?? null, - orgChild1Id: matchedOrgChild1?.id ?? null, - orgChild2Id: matchedOrgChild2?.id ?? null, - orgChild3Id: matchedOrgChild3?.id ?? null, - }, - ); - - for (const x of orgChild4ByChild3.get(data3Id) ?? []) { - const data4Id = x.id; - const matchedOrgChild4 = findMatchedNodeByAncestorDNA(orgChild4Current, x); - await cloneEmployeeNodeBatch( - employeePosMasterByNode.get(getNodeKey("child4", data4Id)) ?? [], - repoEmployeePosmaster, - "posMasterId", - { - orgRootId: matchedOrgRoot?.id ?? null, - orgChild1Id: matchedOrgChild1?.id ?? null, - orgChild2Id: matchedOrgChild2?.id ?? null, - orgChild3Id: matchedOrgChild3?.id ?? null, - orgChild4Id: matchedOrgChild4?.id ?? null, - }, + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, ); + employeePosition.posMasterId = employeePosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + //create employeeTempPosmaster + await Promise.all( + _orgemployeeTempPosMaster + .filter((x: EmployeeTempPosMaster) => x.orgRootId == dataId && x.orgChild1Id == null) + .map(async (item: any) => { + delete item.id; + const employeeTempPosMaster = Object.assign(new EmployeeTempPosMaster(), item); + employeeTempPosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.current_holderId = item.current_holderId; + // } else { + // // employeeTempPosMaster.next_holderId = null; + // employeeTempPosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.authRoleId = item.authRoleId; + // } else { + // employeeTempPosMaster.authRoleId = null; + // } + // employeeTempPosMaster.current_holderId = null; + employeeTempPosMaster.orgRevisionId = orgRevisionDraft.id; + employeeTempPosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeeTempPosMaster.createdUserId = ""; + employeeTempPosMaster.createdFullName = "System Administrator"; + employeeTempPosMaster.createdAt = new Date(); + employeeTempPosMaster.lastUpdateUserId = ""; + employeeTempPosMaster.lastUpdateFullName = "System Administrator"; + employeeTempPosMaster.lastUpdatedAt = new Date(); + await repoEmployeeTempPosmaster.save(employeeTempPosMaster); - await cloneEmployeeNodeBatch( - employeeTempPosMasterByNode.get(getNodeKey("child4", data4Id)) ?? [], - repoEmployeeTempPosmaster, - "posMasterTempId", - { - orgRootId: matchedOrgRoot?.id ?? null, - orgChild1Id: matchedOrgChild1?.id ?? null, - orgChild2Id: matchedOrgChild2?.id ?? null, - orgChild3Id: matchedOrgChild3?.id ?? null, - orgChild4Id: matchedOrgChild4?.id ?? null, - }, + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterTempId = employeeTempPosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // } + + //create org + orgChild1 + .filter((x: OrgChild1) => x.orgRootId == dataId) + .forEach(async (x: any) => { + var data1Id = x.id; + const orgChild1Current = await child1Repository.find({ + where: { orgRevisionId: orgRevisionDraft.id }, + }); + + const matchedOrgChild1 = orgChild1Current.find((i: OrgChild1) => { + if ( + x.ancestorDNA === x.id || // ถ้า ancestorDNA ถูกตั้งเป็น id ตัวเอง + x.ancestorDNA === null || + x.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ) { + return ( + i.ancestorDNA === null || i.ancestorDNA === "00000000-0000-0000-0000-000000000000" ); } - } - } - } - } + return i.ancestorDNA === x.ancestorDNA; + }); + // ("[in case Child1] ancestorDNA", `${x.orgChild1Id == matchedOrgChild1?.id}`); + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + //create employeePosmaster + await Promise.all( + _orgemployeePosMaster + .filter((x: EmployeePosMaster) => x.orgChild1Id == data1Id && x.orgChild2Id == null) + .map(async (item: any) => { + delete item.id; + // console.log("[in case Child1] orgChild1Id == data1Id"); + const employeePosMaster = Object.assign(new EmployeePosMaster(), item); + employeePosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.current_holderId = item.current_holderId; + // } else { + // // employeePosMaster.next_holderId = null; + // employeePosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.authRoleId = item.authRoleId; + // } else { + // employeePosMaster.authRoleId = null; + // } + // employeePosMaster.current_holderId = null; + employeePosMaster.orgRevisionId = orgRevisionDraft.id; + employeePosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeePosMaster.orgChild1Id = matchedOrgChild1?.id ?? null; + employeePosMaster.createdUserId = ""; + employeePosMaster.createdFullName = "System Administrator"; + employeePosMaster.createdAt = new Date(); + employeePosMaster.lastUpdateUserId = ""; + employeePosMaster.lastUpdateFullName = "System Administrator"; + employeePosMaster.lastUpdatedAt = new Date(); + await repoEmployeePosmaster.save(employeePosMaster); - const [employeePosMaster, employeeTempPosMaster] = await Promise.all([ - repoEmployeePosmaster.find({ - where: { orgRevisionId: orgRevisionDraft.id }, - relations: ["positions", "positions.posLevel", "positions.posType"], - }), - repoEmployeeTempPosmaster.find({ - where: { orgRevisionId: orgRevisionDraft.id }, - relations: ["positions", "positions.posLevel", "positions.posType"], - }), - ]); - const profileEmployeeIds = Array.from( - new Set( - [...employeePosMaster, ...employeeTempPosMaster] - .map((item) => item.next_holderId) - .filter((profileId): profileId is string => !!profileId), - ), - ); - const profileEmployeeMap = new Map(); - if (profileEmployeeIds.length > 0) { - const profiles = await repoProfileEmployee.findBy({ - id: In(profileEmployeeIds), - }); - profiles.forEach((profile) => profileEmployeeMap.set(profile.id, profile)); - } - const updatedProfileEmployeeIds = new Set(); - const employeePosMasterIdsToTouch: string[] = []; - const employeeTempPosMasterIdsToTouch: string[] = []; + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterId = employeePosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // create employeeTempPosmaster + await Promise.all( + _orgemployeeTempPosMaster + .filter( + (x: EmployeeTempPosMaster) => x.orgChild1Id == data1Id && x.orgChild2Id == null, + ) + .map(async (item: any) => { + delete item.id; + const employeeTempPosMaster = Object.assign(new EmployeeTempPosMaster(), item); + employeeTempPosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.current_holderId = item.current_holderId; + // } else { + // // employeeTempPosMaster.next_holderId = null; + // employeeTempPosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.authRoleId = item.authRoleId; + // } else { + // employeeTempPosMaster.authRoleId = null; + // } + // employeeTempPosMaster.current_holderId = null; + employeeTempPosMaster.orgRevisionId = orgRevisionDraft.id; + employeeTempPosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeeTempPosMaster.orgChild1Id = matchedOrgChild1?.id ?? null; + employeeTempPosMaster.createdUserId = ""; + employeeTempPosMaster.createdFullName = "System Administrator"; + employeeTempPosMaster.createdAt = new Date(); + employeeTempPosMaster.lastUpdateUserId = ""; + employeeTempPosMaster.lastUpdateFullName = "System Administrator"; + employeeTempPosMaster.lastUpdatedAt = new Date(); + await repoEmployeeTempPosmaster.save(employeeTempPosMaster); + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterTempId = employeeTempPosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // } + + //create org + orgChild2 + .filter((x: OrgChild2) => x.orgChild1Id == data1Id) + .forEach(async (x: any) => { + var data2Id = x.id; + const orgChild2Current = await child2Repository.find({ + where: { orgRevisionId: orgRevisionDraft.id }, + }); + + const matchedOrgChild2 = orgChild2Current.find((i: OrgChild2) => { + if ( + x.ancestorDNA === x.id || // ถ้า ancestorDNA ถูกตั้งเป็น id ตัวเอง + x.ancestorDNA === null || + x.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ) { + return ( + i.ancestorDNA === null || + i.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ); + } + return i.ancestorDNA === x.ancestorDNA; + }); + // console.log("[in case Child2] ancestorDNA", `${x.orgChild2Id == matchedOrgChild2?.id}`); + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + //create employeePosmaster + await Promise.all( + _orgemployeePosMaster + .filter( + (x: EmployeePosMaster) => x.orgChild2Id == data2Id && x.orgChild3Id == null, + ) + .map(async (item: any) => { + delete item.id; + // console.log("[in case Child2] orgChild2Id == data2Id"); + const employeePosMaster = Object.assign(new EmployeePosMaster(), item); + employeePosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.current_holderId = item.current_holderId; + // } else { + // // employeePosMaster.next_holderId = null; + // employeePosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.authRoleId = item.authRoleId; + // } else { + // employeePosMaster.authRoleId = null; + // } + // employeePosMaster.current_holderId = null; + employeePosMaster.orgRevisionId = orgRevisionDraft.id; + employeePosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeePosMaster.orgChild1Id = matchedOrgChild1?.id ?? null; + employeePosMaster.orgChild2Id = matchedOrgChild2?.id ?? null; + employeePosMaster.createdUserId = ""; + employeePosMaster.createdFullName = "System Administrator"; + employeePosMaster.createdAt = new Date(); + employeePosMaster.lastUpdateUserId = ""; + employeePosMaster.lastUpdateFullName = "System Administrator"; + employeePosMaster.lastUpdatedAt = new Date(); + await repoEmployeePosmaster.save(employeePosMaster); + + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterId = employeePosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // create employeeTempPosmaster + await Promise.all( + _orgemployeeTempPosMaster + .filter( + (x: EmployeeTempPosMaster) => + x.orgChild2Id == data2Id && x.orgChild3Id == null, + ) + .map(async (item: any) => { + delete item.id; + const employeeTempPosMaster = Object.assign( + new EmployeeTempPosMaster(), + item, + ); + employeeTempPosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.current_holderId = item.current_holderId; + // } else { + // // employeeTempPosMaster.next_holderId = null; + // employeeTempPosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.authRoleId = item.authRoleId; + // } else { + // employeeTempPosMaster.authRoleId = null; + // } + // employeeTempPosMaster.current_holderId = null; + employeeTempPosMaster.orgRevisionId = orgRevisionDraft.id; + employeeTempPosMaster.orgRootId = dataId; + employeeTempPosMaster.orgChild1Id = data1Id; + employeeTempPosMaster.orgChild2Id = data2Id; + employeeTempPosMaster.createdUserId = ""; + employeeTempPosMaster.createdFullName = "System Administrator"; + employeeTempPosMaster.createdAt = new Date(); + employeeTempPosMaster.lastUpdateUserId = ""; + employeeTempPosMaster.lastUpdateFullName = "System Administrator"; + employeeTempPosMaster.lastUpdatedAt = new Date(); + await repoEmployeeTempPosmaster.save(employeeTempPosMaster); + + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterTempId = employeeTempPosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // } + + //create org + orgChild3 + .filter((x: OrgChild3) => x.orgChild2Id == data2Id) + .forEach(async (x: any) => { + var data3Id = x.id; + const orgChild3Current = await child3Repository.find({ + where: { orgRevisionId: orgRevisionDraft.id }, + }); + + const matchedOrgChild3 = orgChild3Current.find((i: OrgChild3) => { + if ( + x.ancestorDNA === x.id || // ถ้า ancestorDNA ถูกตั้งเป็น id ตัวเอง + x.ancestorDNA === null || + x.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ) { + return ( + i.ancestorDNA === null || + i.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ); + } + return i.ancestorDNA === x.ancestorDNA; + }); + // console.log("[in case Child3] ancestorDNA", `${x.orgChild3Id == matchedOrgChild3?.id}`); + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + //create employeePosmaster + await Promise.all( + _orgemployeePosMaster + .filter( + (x: EmployeePosMaster) => + x.orgChild3Id == data3Id && x.orgChild4Id == null, + ) + .map(async (item: any) => { + delete item.id; + // console.log("[in case Child3] orgChild3Id == data3Id"); + const employeePosMaster = Object.assign(new EmployeePosMaster(), item); + employeePosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.current_holderId = item.current_holderId; + // } else { + // // employeePosMaster.next_holderId = null; + // employeePosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.authRoleId = item.authRoleId; + // } else { + // employeePosMaster.authRoleId = null; + // } + // employeePosMaster.current_holderId = null; + employeePosMaster.orgRevisionId = orgRevisionDraft.id; + employeePosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeePosMaster.orgChild1Id = matchedOrgChild1?.id ?? null; + employeePosMaster.orgChild2Id = matchedOrgChild2?.id ?? null; + employeePosMaster.orgChild3Id = matchedOrgChild3?.id ?? null; + employeePosMaster.createdUserId = ""; + employeePosMaster.createdFullName = "System Administrator"; + employeePosMaster.createdAt = new Date(); + employeePosMaster.lastUpdateUserId = ""; + employeePosMaster.lastUpdateFullName = "System Administrator"; + employeePosMaster.lastUpdatedAt = new Date(); + await repoEmployeePosmaster.save(employeePosMaster); + + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterId = employeePosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // create employeeTempPosmaster + await Promise.all( + _orgemployeeTempPosMaster + .filter( + (x: EmployeeTempPosMaster) => + x.orgChild3Id == data3Id && x.orgChild4Id == null, + ) + .map(async (item: any) => { + delete item.id; + const employeeTempPosMaster = Object.assign( + new EmployeeTempPosMaster(), + item, + ); + employeeTempPosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.current_holderId = item.current_holderId; + // } else { + // // employeeTempPosMaster.next_holderId = null; + // employeeTempPosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.authRoleId = item.authRoleId; + // } else { + // employeeTempPosMaster.authRoleId = null; + // } + // employeeTempPosMaster.current_holderId = null; + employeeTempPosMaster.orgRevisionId = orgRevisionDraft.id; + employeeTempPosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeeTempPosMaster.orgChild1Id = matchedOrgChild1?.id ?? null; + employeeTempPosMaster.orgChild2Id = matchedOrgChild2?.id ?? null; + employeeTempPosMaster.orgChild3Id = matchedOrgChild3?.id ?? null; + employeeTempPosMaster.createdUserId = ""; + employeeTempPosMaster.createdFullName = "System Administrator"; + employeeTempPosMaster.createdAt = new Date(); + employeeTempPosMaster.lastUpdateUserId = ""; + employeeTempPosMaster.lastUpdateFullName = "System Administrator"; + employeeTempPosMaster.lastUpdatedAt = new Date(); + await repoEmployeeTempPosmaster.save(employeeTempPosMaster); + + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterTempId = employeeTempPosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // } + + //create org + orgChild4 + .filter((x: OrgChild4) => x.orgChild3Id == data3Id) + .forEach(async (x: any) => { + var data4Id = x.id; + const orgChild4Current = await child4Repository.find({ + where: { orgRevisionId: orgRevisionDraft.id }, + }); + + const matchedOrgChild4 = orgChild4Current.find((i: OrgChild4) => { + if ( + x.ancestorDNA === x.id || // ถ้า ancestorDNA ถูกตั้งเป็น id ตัวเอง + x.ancestorDNA === null || + x.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ) { + return ( + i.ancestorDNA === null || + i.ancestorDNA === "00000000-0000-0000-0000-000000000000" + ); + } + return i.ancestorDNA === x.ancestorDNA; + }); + // console.log("[in case Child4] ancestorDNA", `${x.orgChild4Id == matchedOrgChild4?.id}`); + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + //create employeePosmaster + await Promise.all( + _orgemployeePosMaster + .filter((x: EmployeePosMaster) => x.orgChild4Id == data4Id) + .map(async (item: any) => { + delete item.id; + // console.log("[in case Child4] orgChild4Id == data4Id"); + const employeePosMaster = Object.assign( + new EmployeePosMaster(), + item, + ); + employeePosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.current_holderId = item.current_holderId; + // } else { + // // employeePosMaster.next_holderId = null; + // employeePosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeePosMaster.authRoleId = item.authRoleId; + // } else { + // employeePosMaster.authRoleId = null; + // } + // employeePosMaster.current_holderId = null; + employeePosMaster.orgRevisionId = orgRevisionDraft.id; + employeePosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeePosMaster.orgChild1Id = matchedOrgChild1?.id ?? null; + employeePosMaster.orgChild2Id = matchedOrgChild2?.id ?? null; + employeePosMaster.orgChild3Id = matchedOrgChild3?.id ?? null; + employeePosMaster.orgChild4Id = matchedOrgChild4?.id ?? null; + employeePosMaster.createdUserId = ""; + employeePosMaster.createdFullName = "System Administrator"; + employeePosMaster.createdAt = new Date(); + employeePosMaster.lastUpdateUserId = ""; + employeePosMaster.lastUpdateFullName = "System Administrator"; + employeePosMaster.lastUpdatedAt = new Date(); + await repoEmployeePosmaster.save(employeePosMaster); + + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterId = employeePosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + //create employeeTempPosmaster + await Promise.all( + _orgemployeeTempPosMaster + .filter((x: EmployeeTempPosMaster) => x.orgChild4Id == data4Id) + .map(async (item: any) => { + delete item.id; + const employeeTempPosMaster = Object.assign( + new EmployeeTempPosMaster(), + item, + ); + employeeTempPosMaster.positions = []; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.current_holderId = item.current_holderId; + // } else { + // // employeeTempPosMaster.next_holderId = null; + // employeeTempPosMaster.isSit = false; + // } + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE" + // ) { + // employeeTempPosMaster.authRoleId = item.authRoleId; + // } else { + // employeeTempPosMaster.authRoleId = null; + // } + // employeeTempPosMaster.current_holderId = null; + employeeTempPosMaster.orgRevisionId = orgRevisionDraft.id; + employeeTempPosMaster.orgRootId = matchedOrgRoot?.id ?? null; + employeeTempPosMaster.orgChild1Id = matchedOrgChild1?.id ?? null; + employeeTempPosMaster.orgChild2Id = matchedOrgChild2?.id ?? null; + employeeTempPosMaster.orgChild3Id = matchedOrgChild3?.id ?? null; + employeeTempPosMaster.orgChild4Id = matchedOrgChild4?.id ?? null; + employeeTempPosMaster.createdUserId = ""; + employeeTempPosMaster.createdFullName = "System Administrator"; + employeeTempPosMaster.createdAt = new Date(); + employeeTempPosMaster.lastUpdateUserId = ""; + employeeTempPosMaster.lastUpdateFullName = "System Administrator"; + employeeTempPosMaster.lastUpdatedAt = new Date(); + await repoEmployeeTempPosmaster.save(employeeTempPosMaster); + + //create employeePosition + item.positions.map(async (pos: any) => { + delete pos.id; + const employeePosition: EmployeePosition = Object.assign( + new EmployeePosition(), + pos, + ); + employeePosition.posMasterTempId = employeeTempPosMaster.id; + // if ( + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION" || + // requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" + // ) { + // employeePosition.positionIsSelected = false; + // } + employeePosition.createdUserId = ""; + employeePosition.createdFullName = "System Administrator"; + employeePosition.createdAt = new Date(); + employeePosition.lastUpdateUserId = ""; + employeePosition.lastUpdateFullName = "System Administrator"; + employeePosition.lastUpdatedAt = new Date(); + await employeePositionRepository.save(employeePosition); + }); + }), + ); + // } + }); + }); + }); + }); + }); + // } + + const employeePosMaster = await repoEmployeePosmaster.find({ + where: { orgRevisionId: orgRevisionDraft.id }, + relations: ["positions", "positions.posLevel", "positions.posType"], + }); for (const item of employeePosMaster) { if (item.next_holderId != null) { - const profile = profileEmployeeMap.get(item.next_holderId); - const position = item.positions.find((x) => x.positionIsSelected == true); + const profile = await repoProfileEmployee.findOne({ + where: { id: item.next_holderId == null ? "" : item.next_holderId }, + }); + const position = await item.positions.find((x) => x.positionIsSelected == true); const _null: any = null; if (profile != null) { profile.posLevelId = position?.posLevelId ?? _null; profile.posTypeId = position?.posTypeId ?? _null; profile.position = position?.positionName ?? _null; - updatedProfileEmployeeIds.add(profile.id); + await repoProfileEmployee.save(profile); } } - employeePosMasterIdsToTouch.push(item.id); + // item.current_holderId = item.next_holderId; + // item.next_holderId = null; + item.lastUpdateUserId = lastUpdateUserId; + item.lastUpdateFullName = lastUpdateFullName; + item.lastUpdatedAt = lastUpdatedAt; + await repoEmployeePosmaster.save(item).catch((e) => console.log(e)); } + const employeeTempPosMaster = await repoEmployeeTempPosmaster.find({ + where: { orgRevisionId: orgRevisionDraft.id }, + relations: ["positions", "positions.posLevel", "positions.posType"], + }); for (const item of employeeTempPosMaster) { if (item.next_holderId != null) { - const profile = profileEmployeeMap.get(item.next_holderId); - const position = item.positions.find((x) => x.positionIsSelected == true); + const profile = await repoProfileEmployee.findOne({ + where: { id: item.next_holderId == null ? "" : item.next_holderId }, + }); + const position = await item.positions.find((x) => x.positionIsSelected == true); const _null: any = null; if (profile != null) { profile.posLevelId = position?.posLevelId ?? _null; profile.posTypeId = position?.posTypeId ?? _null; profile.position = position?.positionName ?? _null; - updatedProfileEmployeeIds.add(profile.id); + await repoProfileEmployee.save(profile); } } - employeeTempPosMasterIdsToTouch.push(item.id); + // item.current_holderId = item.next_holderId; + // item.next_holderId = null; + item.lastUpdateUserId = lastUpdateUserId; + item.lastUpdateFullName = lastUpdateFullName; + item.lastUpdatedAt = lastUpdatedAt; + await repoEmployeeTempPosmaster.save(item).catch((e) => console.log(e)); } - if (employeePosMasterIdsToTouch.length > 0) { - const chunks = chunkArray(employeePosMasterIdsToTouch, 500); - const employeePosMasterTableName = repoEmployeePosmaster.metadata.tableName; - for (const chunk of chunks) { - const wherePlaceholders = chunk.map(() => "?").join(", "); - await manager.query( - `UPDATE \`${employeePosMasterTableName}\` - SET lastUpdateUserId = ?, - lastUpdateFullName = ?, - lastUpdatedAt = ? - WHERE id IN (${wherePlaceholders})`, - [lastUpdateUserId, lastUpdateFullName, lastUpdatedAt, ...chunk], - ); - } - } - if (employeeTempPosMasterIdsToTouch.length > 0) { - const chunks = chunkArray(employeeTempPosMasterIdsToTouch, 500); - const employeeTempPosMasterTableName = repoEmployeeTempPosmaster.metadata.tableName; - for (const chunk of chunks) { - const wherePlaceholders = chunk.map(() => "?").join(", "); - await manager.query( - `UPDATE \`${employeeTempPosMasterTableName}\` - SET lastUpdateUserId = ?, - lastUpdateFullName = ?, - lastUpdatedAt = ? - WHERE id IN (${wherePlaceholders})`, - [lastUpdateUserId, lastUpdateFullName, lastUpdatedAt, ...chunk], - ); - } - } - if (updatedProfileEmployeeIds.size > 0) { - const profilesToSave = Array.from(updatedProfileEmployeeIds) - .map((profileId) => profileEmployeeMap.get(profileId)) - .filter((profile): profile is ProfileEmployee => !!profile); - const chunks = chunkArray(profilesToSave, 200); - for (const chunk of chunks) { - await repoProfileEmployee.save(chunk); - } - } - console.timeEnd("[AMQ] clone_org_structure"); - - console.time("[AMQ] save_revision_status"); - orgRevisionPublish.orgRevisionIsDraft = false; - orgRevisionPublish.orgRevisionIsCurrent = false; - await repoOrgRevision.save(orgRevisionPublish); - - orgRevisionDraft.orgRevisionIsCurrent = true; - orgRevisionDraft.orgRevisionIsDraft = false; - await repoOrgRevision.save(orgRevisionDraft); - console.timeEnd("[AMQ] save_revision_status"); - }); - - if (shouldSkipPublishInTransaction) { - console.log( - `[AMQ] Skip publish in transaction: revision ${id} state changed before write phase`, - ); - console.timeEnd("[AMQ] handler_org_total"); - return true; } - console.log("[AMQ] Excecute Organization Success"); if (user) { sendWebSocket( @@ -1673,15 +1741,9 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { { userId: user?.sub }, ).catch(console.error); } - - console.log(`[AMQ] handler_org SUCCESS - Total time: ${Date.now() - startTime}ms`); - console.timeEnd("[AMQ] handler_org_total"); - - await clearMenuAndRoleCache(); return true; } catch (error) { - const totalTime = Date.now() - startTime; - console.error(`[AMQ] handler_org ERROR after ${totalTime}ms:`, error); + console.error(error); if (user) { sendWebSocket( "send-publish-org", @@ -1692,37 +1754,10 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { { userId: user?.sub }, ).catch(console.error); } - console.timeEnd("[AMQ] handler_org_total"); return false; } } -async function clearMenuAndRoleCache(): Promise { - const redisClient = redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - - const keysAsync = promisify(redisClient.keys).bind(redisClient); - const delAsync = promisify(redisClient.del).bind(redisClient); - - try { - const menuKeys = await keysAsync("menu_*"); - if (menuKeys.length > 0) { - await delAsync(...menuKeys); - console.log(`[AMQ] Cleared ${menuKeys.length} menu cache keys`); - } - - const roleKeys = await keysAsync("role_*"); - if (roleKeys.length > 0) { - await delAsync(...roleKeys); - console.log(`[AMQ] Cleared ${roleKeys.length} role cache keys`); - } - } finally { - redisClient.quit(); - } -} - async function handler_org_draft(msg: amqp.ConsumeMessage): Promise { const { data, token, user } = JSON.parse(msg.content.toString()); const { requestBody, request, revision } = data; @@ -2393,8 +2428,7 @@ async function handler_org_draft(msg: amqp.ConsumeMessage): Promise { }); await posMasterAssignRepository.delete({ posMasterId: In(_posMasters.map((x) => x.id)) }); await posMasterActRepository.delete({ posMasterId: In(_posMasters.map((x) => x.id)) }); //ใช้ posMasterId ของ revision: draft *แต่ยังไม่เจอช็อดไหนที่ใช้โครงสร้างแบบร่างในรักษาการแทน - await posMasterActRepository.delete({ - //ใช้ posMasterId ของ revision: draft *แต่ยังไม่เจอช็อดไหนที่ใช้โครงสร้างแบบร่างในรักษาการแทน + await posMasterActRepository.delete({ //ใช้ posMasterId ของ revision: draft *แต่ยังไม่เจอช็อดไหนที่ใช้โครงสร้างแบบร่างในรักษาการแทน posMasterChildId: In(_posMasters.map((x) => x.id)), }); // await posMasterRepository.remove(_posMasters); @@ -2422,26 +2456,24 @@ async function handler_org_draft(msg: amqp.ConsumeMessage): Promise { await child2Repository.delete({ orgRevisionId: In(_orgRevisions.map((x) => x.id)) }); await child1Repository.delete({ orgRevisionId: In(_orgRevisions.map((x) => x.id)) }); // Task #2160 อัพเดทหน้าที่จัดการโครงสร้างแบบร่าง - if ( - [ - "ORG", - "ORG_POSITION", - "ORG_POSITION_PERSON", - "ORG_POSITION_ROLE", - "ORG_POSITION_PERSON_ROLE", - ].includes(requestBody.typeDraft?.toUpperCase()) - ) { + if (["ORG", "ORG_POSITION", "ORG_POSITION_PERSON", "ORG_POSITION_ROLE", "ORG_POSITION_PERSON_ROLE"].includes(requestBody.typeDraft?.toUpperCase())) { const _newRoots = await orgRootRepository.find({ - where: { orgRevisionId: revision.id }, + where: { orgRevisionId: revision.id } }); - const newRootMap = new Map(_newRoots.map((r) => [r.ancestorDNA, r.id])); + const newRootMap = new Map( + _newRoots.map(r => [r.ancestorDNA, r.id]) + ); for (const oldRoot of _roots) { const newRootId = newRootMap.get(oldRoot.ancestorDNA); if (!newRootId) continue; // อัพเดท orgRootId ที่อยู่ภายใต้ orgRevision แบบร่างเดิมเป็นของ orgRevision แบบร่างใหม่ - await permissionOrgRepository.update({ orgRootId: oldRoot.id }, { orgRootId: newRootId }); + await permissionOrgRepository.update( + { orgRootId: oldRoot.id }, + { orgRootId: newRootId } + ); } - } else { + } + else { await permissionOrgRepository.delete({ orgRootId: In(_roots.map((x) => x.id)), }); diff --git a/src/services/webSocket.ts b/src/services/webSocket.ts index 7d464655..36359cd8 100644 --- a/src/services/webSocket.ts +++ b/src/services/webSocket.ts @@ -22,7 +22,7 @@ export function initWebSocket() { }); io.on("connection", (ws) => { - // console.log("✅ Client connected to WebSocket"); + console.log("✅ Client connected to WebSocket"); ws.on("close", () => { console.log("❌ Client disconnected"); @@ -46,7 +46,7 @@ export async function sendWebSocket( ) { if (!io) initWebSocket(); // console.log( `🔔 :`,data.message); - + for (let [id, session] of io.of("/").sockets) { const user: { sub: string; diff --git a/src/utils/org-formatting.ts b/src/utils/org-formatting.ts index eb4b7a9d..701fb478 100644 --- a/src/utils/org-formatting.ts +++ b/src/utils/org-formatting.ts @@ -68,47 +68,3 @@ export function filterPosMasters( ): PosMaster[] { return posMasters.filter((x) => x[childLevelIdKey] == null && x.isDirector === true); } - -/** - * สร้าง orgShortName จาก posMaster (ต้อง load org relations มาก่อน) - */ -export function getOrgShortName(posMaster: PosMaster): string { - if (posMaster.orgChild1Id === null) { - return posMaster.orgRoot?.orgRootShortName ?? ""; - } else if (posMaster.orgChild2Id === null) { - return posMaster.orgChild1?.orgChild1ShortName ?? ""; - } else if (posMaster.orgChild3Id === null) { - return posMaster.orgChild2?.orgChild2ShortName ?? ""; - } else if (posMaster.orgChild4Id === null) { - return posMaster.orgChild3?.orgChild3ShortName ?? ""; - } else { - return posMaster.orgChild4?.orgChild4ShortName ?? ""; - } -} - -/** - * สร้างชื่อสังกัดเต็ม จาก posMaster (join ด้วย \n) - */ -export function getOrgFullName(posMaster: PosMaster): string { - const parts = [ - posMaster.orgChild4?.orgChild4Name, - posMaster.orgChild3?.orgChild3Name, - posMaster.orgChild2?.orgChild2Name, - posMaster.orgChild1?.orgChild1Name, - posMaster.orgRoot?.orgRootName, - ]; - return parts.filter((part) => part !== undefined && part !== null).join("\n"); -} - -/** - * สร้างเลขที่ตำแหน่ง เช่น "กทม. กบ. 1234 ช" - */ -export function getPosMasterNo(posMaster: PosMaster): string { - const orgShortName = getOrgShortName(posMaster); - const parts = [ - posMaster.posMasterNoPrefix, - posMaster.posMasterNo, - posMaster.posMasterNoSuffix, - ].filter((part) => part !== null && part !== undefined); - return `${orgShortName} ${parts.join(' ')}`; -} diff --git a/src/utils/tenure.ts b/src/utils/tenure.ts deleted file mode 100644 index 1c97dff1..00000000 --- a/src/utils/tenure.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Normalize a duration sum using calendar arithmetic - * Converts excess days to months using average month length (30.4375 days) - * and excess months to years. Matches the logic used in stored procedures. - * - * @param years Total years from sum - * @param months Total months from sum - * @param days Total days from sum - * @returns Normalized { years, months, days } - */ -export function normalizeDurationSumSimple( - years: number, - months: number, - days: number, -): { years: number; months: number; days: number } { - const DAYS_PER_MONTH = 30.4375; // Average days per month in Gregorian calendar - - let totalMonths = months; - let totalDays = days; - - // Convert excess days to months - if (totalDays >= DAYS_PER_MONTH) { - const additionalMonths = Math.floor(totalDays / DAYS_PER_MONTH); - totalMonths += additionalMonths; - totalDays = totalDays - additionalMonths * DAYS_PER_MONTH; - } - - // Convert excess months to years - let totalYears = years; - if (totalMonths >= 12) { - const additionalYears = Math.floor(totalMonths / 12); - totalYears += additionalYears; - totalMonths = totalMonths % 12; - } - - return { years: totalYears, months: Math.floor(totalMonths), days: Math.floor(totalDays) }; -}