Skip to content

12.2 · Leave (การลา)

บทนี้เจาะลึก business logic ของระบบการลา — state machine, multi-level approval, validation, และการส่งต่อไป cu-central-api เพื่อคำนวณยอดคงเหลือ


ภาพรวม

ด้านค่า
ไฟล์ controller หลักlib/controllers/hrmi/hrmi.js (~2,600 บรรทัด)
Repositorylib/repositories/hrmi/hrmiLeave.js, history.js
Forward controllercentralize/controllers/hrmi.js
Status enum7 สถานะ (config.js:430-458)
Approvalmulti-level (configurable ตามประเภทการลา)
Cross-serviceคำนวณยอดลา, วันทำการ, รายชื่อ approver ผ่าน cu-central-api

State machine

                createLeaveDocument


   ┌────────────── [DRAF 'D'] ◄──── createUpdateLeaveDocumentDraf
   │                    │
   │                    │ submit (approveStatus from client)
   │                    ▼
   │                [PENDING 'W'] ────────┐
   │                    │                  │
   │                    │ approve lv.1     │ reject
   │                    ▼                  ▼
   │            [PENDING lv.2]        [REJECT 'N']
   │                    │
   │                    │ approve lv.2
   │                    ▼
   │              (ถ้ามี lv.3, 4, ...)
   │                    │
   │                    │ approve last level
   │                    ▼
   │               [APPROVE 'Y']

   └── cancel (removeLeaveDocumentDraf) ──► status='CANCEL' (soft delete)
   
   ทุกสถานะ (ยกเว้น APPROVE) ──── cancel ──► [CANCEL 'C']

Status mapping

430:458:vtrc-api/api/src/config.js
  statusHrmi = [
    { statusId: 'W', statusLabel: 'PENDING' },
    { statusId: 'Y', statusLabel: 'APPROVE' },
    { statusId: 'N', statusLabel: 'REJECT' },
    { statusId: 'C', statusLabel: 'CANCEL' },
    { statusId: 'CR', statusLabel: 'CREATE' },
    { statusId: 'A', statusLabel: 'ALL' },
    { statusId: 'D', statusLabel: 'DRAF' }
  ],

1. การสร้างเอกสาร — createLeaveDocument

ที่อยู่: hrmi.js:1219

Flow

1. รับ args จาก client:
   - empCode, documentDate, leaveTypeId
   - leaveStartDate, leaveEndDate, durationType, leaveDay
   - description, approveStatus (DRAF หรือ PENDING)
   - approveEmpCode, documentNo (optional)

2. validateDuplicateDate — ตรวจวันที่ซ้ำกับเอกสารเดิม
   validateDuplicateTime — ตรวจเวลาซ้ำ (กรณีลาเป็นชั่วโมง)

3. เรียก cu-central-api ดึง config ประเภทการลา:
   hrmiGetLeaveTypeCD → min/max day, early/back day, reqAttachDay

4. validate ตาม config (ดูหัวข้อ "Validation" ด้านล่าง)

5. ถ้า reqAttachDay และลาต่อเนื่อง ≥ N วัน → ตรวจใบรับรองแพทย์
   findLeaveContinuos

6. คำนวณ leaveDay คงเหลือ (จาก CD):
   hrmiGetLeaveDayCD → remainLeaveDay, leaveDay, currentLeaveDay

7. generate docNo format `LEV[YYMM]-[6-digit]`

8. INSERT LeaveDocument + INSERT LeaveHistory (status=CREATE)

9. ถ้า approveStatus === 'W' (PENDING):
   a. ดึงรายชื่อ approver (hrmiGetLeaveApproverCD)
   b. INSERT ทุก approver ลง MasterApproveLeave
   c. notify approver ของ level 1

10. autoApprove branch (เฉพาะเอกสารบางประเภท) → อนุมัติอัตโนมัติ

ที่มา: hrmi.js:1219-1452


2. Multi-level approval — updateLeaveDocument

ที่อยู่: hrmi.js:1753

Flow (approve case)

js
// hrmi.js:1886-1916 (concept)
if (args.approveStatus === "Y") {
  // 1. update approver record ปัจจุบัน
  await approver.updateAction({ ...status:'Y', empCode:approver })
  
  // 2. ลบ approver record ที่เหลือใน level ปัจจุบัน
  await approver.destroyByCondition({ where:{ taskId, level:currentLevel } })
  
  // 3. ตรวจว่ามี level ถัดไปหรือไม่
  const countApprover = await approver.findByConditions({
    where: { taskId, level: currentLevel + 1 }
  })
  
  if (countApprover.length > 0) {
    // 4a. มี level ถัดไป → เลื่อนเอกสารไป PENDING level ถัดไป
    setData = { 
      level: currentLevel + 1, 
      status: "PENDING", 
      approveStatus: "W" 
    }
    // notify approvers ของ level ถัดไป
  } else {
    // 4b. ไม่มี → APPROVE สำเร็จ
    setData = { 
      level: currentLevel + 1, 
      status: "APPROVE", 
      approveStatus: "Y" 
    }
    // ส่งเลขเอกสารไป cu-central-api (hrmiEditLeaveDocumentCD)
    // notify เจ้าของเอกสาร
  }
}

Flow (reject case)

js
if (args.approveStatus === "N") {
  setData = { status: "REJECT", approveStatus: "N" }
  // notify เจ้าของเอกสาร "ไม่อนุมัติ"
}

3. รายชื่อ approver — มาจาก cu-central-api

vtrc-api ไม่ได้เก็บ config ว่าใครเป็น approver ของใคร — ส่งต่อไป cu-central-api ทั้งหมด

56:62:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
export const getApprover = async (docType, context) => {
    const tokenCD = await getTokenCDByEmpCode(context, null)
    const getApprover = await hrmiGetApproverCD(tokenCD, { empCode: context.empCode, docType })
    return getApprover.getApprover
}

รายชื่อถูกส่งกลับเป็นโครงสร้าง:

js
[
  {
    users: [{ label:"คุณก.", value:"emp001" }, { label:"คุณข.", value:"emp002" }],
    level: 1,
    approveType: "APPROVE",
    canEdit: true
  },
  {
    users: [{ label:"คุณค.", value:"emp003" }],
    level: 2,
    approveType: "APPROVE",
    canEdit: false
  }
]

4. Validation — createLeaveDocument

ที่อยู่: hrmi.js:1283-1332

Validation rules

Ruleความหมายข้อยกเว้น
maxLeaveDayลาครั้งเดียวห้ามเกิน N วัน
minLeaveDayลาครั้งเดียวอย่างน้อย N วัน
earlyLeaveDayต้องลาล่วงหน้า N วันสังกัด 0734 ข้าม
backLeaveDayลาย้อนหลังได้ไม่เกิน N วันสังกัด 0734 ข้าม
reqAttachDayลาต่อเนื่อง ≥ N วัน → ต้องแนบใบรับรองแพทย์
validateDuplicateDateวันที่ต้องไม่ซ้ำกับเอกสารเดิม

ตัวอย่างโค้ด (org 0734 ข้าม validation)

js
// hrmi.js:1302 (concept)
if (adminDetail.orgUnitCode.substring(0, 4) !== "0734") {
  // ตรวจ earlyLeaveDay, backLeaveDay
  if (daysBefore < earlyLeaveDay) throw responError('EARLY_LEAVE_DAY')
  if (daysAfter > backLeaveDay) throw responError('BACK_LEAVE_DAY')
}

Leave continuous check

js
// hrmi.js:1164 — findLeaveContinuos
// ตรวจว่ามีการลาต่อเนื่องก่อน/หลังเอกสารนี้หรือไม่
// ถ้ามี → รวมวัน แล้วเช็คว่าเกิน reqAttachDay หรือไม่
// ถ้าเกิน → บังคับแนบใบรับรองแพทย์

5. การคำนวณวันลา / วันทำการ — อยู่ใน cu-central-api

vtrc-api ไม่ได้คำนวณเอง — ส่งต่อไป cu-central-api ทั้งหมด

Get leave day (คงเหลือ)

1517:1540:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
// concept
const result = await hrmiGetLeaveDayCD(tokenCD, {
  empCode,
  reqLeaveDate: leaveStartDate,
  leaveTypeId
})
// return { remainLeaveDay, leaveDay, currentLeaveDay }

Get working day (วันทำการ)

728:750:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
export const getWorkingDay = async (context, empCode, leaveTypeId, leaveStartDate, leaveEndDate) => {
  const { checkedEmpCode, tokenCD } = await getTokenCDByEmpCode(context, empCode)
  // ...
  const result = await hrmiGetWorkingDayCD(tokenCD, {
    empCode: checkedEmpCode,
    leaveTypeId,
    leaveStartDate,
    leaveEndDate
  })
  return result.getWorkingDay
}

cu-central-api ทำการนับ holiday + weekend + เฉพาะวันทำการ — vtrc-api ไม่รู้ logic นี้เลย


6. autoApprove — อนุมัติอัตโนมัติ

กรณีเอกสารบางประเภท (เช่น ลากิจ 1 วัน) อาจตั้งค่าให้อนุมัติอัตโนมัติ:

1640:1691:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
// concept
if (autoApproveFlag) {
  // ข้ามขั้นตอน notify approver
  // อัปเดต status เป็น APPROVE ทันที
  // ส่งเลขเอกสารไป cu-central-api
  // notify เจ้าของ "อนุมัติแล้ว"
}

7. DocNo generation — format LEV[YYMM]-[6-digit]

1424:1452:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
// concept
const generateLeaveDocNo = async () => {
  const yy = (now.getFullYear() + 543).toString().slice(-2)  // พศ.
  const mm = (now.getMonth() + 1).toString().padStart(2, '0')
  const prefix = `LEV${yy}${mm}-`
  
  // ดึง running ล่าสุดของเดือนนี้
  const lastDoc = await LeaveDocument.findOne({
    where: { documentNo: { [Op.like]: `${prefix}%` } },
    order: [['documentNo', 'DESC']]
  })
  
  const running = lastDoc 
    ? parseInt(lastDoc.documentNo.slice(-6)) + 1 
    : 1
  
  return `${prefix}${running.toString().padStart(6, '0')}`
}

ข้อระวัง: race condition — ถ้า 2 request เข้ามาพร้อมกัน อาจได้เลขซ้ำ


8. Plasma branch — เงื่อนไขพิเศษสังกัด 07xx

พนักงานสังกัดศูนย์พลาสมา (org unit code ขึ้นต้นด้วย 07) มี dropdown ประเภทการลาที่แตกต่างจากคนอื่น:

210:220:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
// concept
if (adminDetail.orgUnitCode.substring(0, 2) === '07') {
  // ใช้ dropdown พิเศษสำหรับ plasma
} else {
  // dropdown ปกติ
}

KEY_PLASMA config ใน config.js ระบุว่า division ใดบ้างที่เป็น plasma


9. Notification triggers

ทุก action ส่ง notification ผ่าน sendNotificationByThirdParty:

ActionNotify ใครTitle
submit → PENDING lv.1approvers lv.1"รออนุมัติการลา"
approve → PENDING lv.2approvers lv.2"รออนุมัติการลา"
approve final → APPROVEเจ้าของเอกสาร"อนุมัติการลาแล้ว"
reject → REJECTเจ้าของเอกสาร"ไม่อนุมัติการลา"
cancel → CANCEL(ไม่ notify)

ตัวอย่าง:

js
// hrmi.js:1628-1638 (concept)
await sendNotificationByThirdParty({
  title: "รออนุมัติการลา",
  body: `เอกสารการลาเลขที่ ${docNo} รอการอนุมัติ...`,
  redirect_path: `Leave/Confirm/${docId}`,
  notificationGroup: "LEAVE",
  users: approvers.map(empCode => ({ empCode }))
}, context, "notification")

10. Bug ที่พบ — createLeaveDocument เงื่อนไขกลับด้าน

1266:1275:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
    if (typeof args.approveStatus === "undefined") {   // ← BUG
        mappingStatus = statusHrmi.filter(value => args.approveStatus === value.statusId)
        if (mappingStatus.length < 1) {
            ...
            responError('DATA_NOT_FOUND', {}, null, "Status not found")
        }
        currentStatus = mappingStatus[0].statusLabel
    } else {
        currentStatus = args.status    // ← args.status อาจเป็น undefined จริงๆ
    }

ปัญหา: เงื่อนไข typeof args.approveStatus === "undefined" กลับด้าน — ควรเป็น !== "undefined"

ผลกระทบ: เมื่อ client ส่ง approveStatus มา จะตก else branch และใช้ args.status (ซึ่งอาจเป็น undefined) ทำให้ status ออกมาผิด

ดูรายละเอียดใน บท 11.11


11. Cross-service calls ทั้งหมด

vtrc-api functioncu-central-api queryวัตถุประสงค์
hrmiGetMedicalCDgetMedicalดึงข้อมูลสิทธิ์รักษาพยาบาล
hrmiGetApproverCDgetApproverดึงรายชื่อ approver ตาม docType
hrmiGetLeaveApproverCDgetLeaveApproverดึงรายชื่อ approver พร้อมเช็ค leaveDay
hrmiGetLeaveTypeCDgetLeaveTypeดึง config ประเภทการลา
hrmiGetLeaveDocumentListCDgetLeaveDocumentListดึงรายการใบลา (สำหรับ display)
hrmiGetLeaveDayCDgetLeaveDayคำนวณวันลาคงเหลือ
hrmiGetWorkingDayCDgetWorkingDayคำนวณวันทำการ
hrmiCreateLeaveDocumentCDcreateLeaveDocumentsync เอกสารไป CD
hrmiEditLeaveDocumentCDeditLeaveDocumentsync สถานะเอกสารไป CD
hrmiDeleteLeaveDocumentCDdeleteLeaveDocumentsync การลบไป CD
fetchCDLeaveShiftTimegetLeaveShiftTimeดึงเวลาลาแบบชั่วโมง
getEmpCodeWithOrgUnitCDgetEmployeeOrgUnitดึง org unit ของพนักงาน

ที่อยู่: centralize/controllers/hrmi.js:8-150


12. Magic numbers / hardcoded values

Valueความหมายที่มา
'0734'สังกัดพิเศษ ข้าม validation early/backhrmi.js:1302
'07'Plasma branch prefixhrmi.js:210
['L-001','L-002','L-004','L-005','L-007']leave types ที่อนุญาตให้พนักงานยืนคำขอ (commented out ไม่ได้ใช้)hrmi.js:172
KEY_PLASMAconfig division codes ที่เป็น plasmaconfig.js
พศ. (+ 543)ปีพุทธศักราชหลายจุด

13. Dead code — auto-cancel leave (ถูกปิด)

29:70:vtrc-api/api/src/crons.js
// concept — ถูก comment ทิ้ง
// schedule.scheduleJob('0 1 16 * *', async () => {
//   // ทุกวันที่ 16 ของเดือน เวลา 01:00
//   // ยกเลิกเอกสารลา PENDING ของสังกัด 0734 อัตโนมัติ
//   await hrmi.updateLeaveDocument(..., { 
//     approveStatus: 'C', 
//     noteApprove: 'ยกเลิกโดยระบบ' 
//   })
// })

ถูกปิดไว้ — แต่อาจมีกรณีใช้งานจริง ควรยืนยันกับ HR ก่อนลบ


14. Summary — leave domain

ด้านทำใน vtrc-apiส่งต่อ cu-central-api
State machineใช่
Multi-level approval logicใช่ (ระดับการเลื่อน)
รายชื่อ approverใช่ (config)
Validation rulesใช่ (ใช้ config จาก CD)
คำนวณวันลาคงเหลือใช่
คำนวณวันทำการใช่
แนบไฟล์ใช่
Notificationใช่
DocNo generationใช่
Sync เอกสารใช่ (ส่งไป CD)รับ

ขั้นตอนถัดไป

ไป บท 11.3 Welfare / WDHospital