12.2 · Leave (การลา)
บทนี้เจาะลึก business logic ของระบบการลา — state machine, multi-level approval, validation, และการส่งต่อไป cu-central-api เพื่อคำนวณยอดคงเหลือ
ภาพรวม
| ด้าน | ค่า |
|---|---|
| ไฟล์ controller หลัก | lib/controllers/hrmi/hrmi.js (~2,600 บรรทัด) |
| Repository | lib/repositories/hrmi/hrmiLeave.js, history.js |
| Forward controller | centralize/controllers/hrmi.js |
| Status enum | 7 สถานะ (config.js:430-458) |
| Approval | multi-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
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)
// 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)
if (args.approveStatus === "N") {
setData = { status: "REJECT", approveStatus: "N" }
// notify เจ้าของเอกสาร "ไม่อนุมัติ"
}3. รายชื่อ approver — มาจาก cu-central-api
vtrc-api ไม่ได้เก็บ config ว่าใครเป็น approver ของใคร — ส่งต่อไป cu-central-api ทั้งหมด
export const getApprover = async (docType, context) => {
const tokenCD = await getTokenCDByEmpCode(context, null)
const getApprover = await hrmiGetApproverCD(tokenCD, { empCode: context.empCode, docType })
return getApprover.getApprover
}รายชื่อถูกส่งกลับเป็นโครงสร้าง:
[
{
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)
// 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
// hrmi.js:1164 — findLeaveContinuos
// ตรวจว่ามีการลาต่อเนื่องก่อน/หลังเอกสารนี้หรือไม่
// ถ้ามี → รวมวัน แล้วเช็คว่าเกิน reqAttachDay หรือไม่
// ถ้าเกิน → บังคับแนบใบรับรองแพทย์5. การคำนวณวันลา / วันทำการ — อยู่ใน cu-central-api
vtrc-api ไม่ได้คำนวณเอง — ส่งต่อไป cu-central-api ทั้งหมด
Get leave day (คงเหลือ)
// concept
const result = await hrmiGetLeaveDayCD(tokenCD, {
empCode,
reqLeaveDate: leaveStartDate,
leaveTypeId
})
// return { remainLeaveDay, leaveDay, currentLeaveDay }Get working day (วันทำการ)
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 วัน) อาจตั้งค่าให้อนุมัติอัตโนมัติ:
// concept
if (autoApproveFlag) {
// ข้ามขั้นตอน notify approver
// อัปเดต status เป็น APPROVE ทันที
// ส่งเลขเอกสารไป cu-central-api
// notify เจ้าของ "อนุมัติแล้ว"
}7. DocNo generation — format LEV[YYMM]-[6-digit]
// 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 ประเภทการลาที่แตกต่างจากคนอื่น:
// 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:
| Action | Notify ใคร | Title |
|---|---|---|
| submit → PENDING lv.1 | approvers lv.1 | "รออนุมัติการลา" |
| approve → PENDING lv.2 | approvers lv.2 | "รออนุมัติการลา" |
| approve final → APPROVE | เจ้าของเอกสาร | "อนุมัติการลาแล้ว" |
| reject → REJECT | เจ้าของเอกสาร | "ไม่อนุมัติการลา" |
| cancel → CANCEL | (ไม่ notify) | — |
ตัวอย่าง:
// 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 เงื่อนไขกลับด้าน
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 function | cu-central-api query | วัตถุประสงค์ |
|---|---|---|
hrmiGetMedicalCD | getMedical | ดึงข้อมูลสิทธิ์รักษาพยาบาล |
hrmiGetApproverCD | getApprover | ดึงรายชื่อ approver ตาม docType |
hrmiGetLeaveApproverCD | getLeaveApprover | ดึงรายชื่อ approver พร้อมเช็ค leaveDay |
hrmiGetLeaveTypeCD | getLeaveType | ดึง config ประเภทการลา |
hrmiGetLeaveDocumentListCD | getLeaveDocumentList | ดึงรายการใบลา (สำหรับ display) |
hrmiGetLeaveDayCD | getLeaveDay | คำนวณวันลาคงเหลือ |
hrmiGetWorkingDayCD | getWorkingDay | คำนวณวันทำการ |
hrmiCreateLeaveDocumentCD | createLeaveDocument | sync เอกสารไป CD |
hrmiEditLeaveDocumentCD | editLeaveDocument | sync สถานะเอกสารไป CD |
hrmiDeleteLeaveDocumentCD | deleteLeaveDocument | sync การลบไป CD |
fetchCDLeaveShiftTime | getLeaveShiftTime | ดึงเวลาลาแบบชั่วโมง |
getEmpCodeWithOrgUnitCD | getEmployeeOrgUnit | ดึง org unit ของพนักงาน |
ที่อยู่: centralize/controllers/hrmi.js:8-150
12. Magic numbers / hardcoded values
| Value | ความหมาย | ที่มา |
|---|---|---|
'0734' | สังกัดพิเศษ ข้าม validation early/back | hrmi.js:1302 |
'07' | Plasma branch prefix | hrmi.js:210 |
['L-001','L-002','L-004','L-005','L-007'] | leave types ที่อนุญาตให้พนักงานยืนคำขอ (commented out ไม่ได้ใช้) | hrmi.js:172 |
KEY_PLASMA | config division codes ที่เป็น plasma | config.js |
พศ. (+ 543) | ปีพุทธศักราช | หลายจุด |
13. Dead code — auto-cancel leave (ถูกปิด)
// 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) | รับ |