18 KiB
Employee Training History Audit Report
Summary
Overall Status
- Good
ภาพรวม:
- หน้า
Training Recordsเดินตามโครงสร้างหลักของโปรเจ็กต์ค่อนข้างดี โดยใช้PageContainer, sharedDataTable, React Query, local route handlers, และ server-side organization scoping ตามแพตเทิร์นของtraining-records - ฝั่ง server มีการบังคับสิทธิ์หลักถูกต้อง ทั้ง list, detail, create, update, delete และ review โดย employee เห็นได้เฉพาะข้อมูลของตนเอง ส่วน HRD/admin เห็นได้ระดับ organization
- จุดที่ควรปรับหลักอยู่ที่ความสอดคล้องระหว่าง UI กับ permission จริง, ความชัดเจนของ detail/edit flow, และการเปิดใช้ capability ฝั่ง UI ให้ครบกับ business rule เรื่อง historical filtering
Architecture Review
สิ่งที่พบ:
- route หลักอยู่ที่
src/app/dashboard/training-records/page.tsx - list page ใช้ server prefetch ผ่าน
src/features/training-records/components/training-record-listing.tsx - table ใช้ shared
DataTable,DataTableToolbar,useDataTable - data source ใช้
trainingRecordsQueryOptions()->api/service.ts->/api/training-records - persistence และ access control อยู่ที่
src/app/api/training-records/route.ts,src/app/api/training-records/[id]/route.ts, และsrc/features/training-records/server/training-record-data.ts - detail route reuse form page ผ่าน
src/features/training-records/components/training-record-view-page.tsx
ข้อสรุป:
- โครงสร้างโดยรวมสอดคล้องกับเอกสาร
AI_DEVELOPMENT_GUIDEและ canonical feature ของโปรเจ็กต์ - อย่างไรก็ดี semantic ของหน้า detail/edit ยังไม่ชัด เพราะ route เดียวกันถูกใช้ทั้ง "ดูรายละเอียด" และ "แก้ไข"
Functional Review
| Feature | Status | Notes |
|---|---|---|
| View own training history | Pass | page ใช้ requireEmployeeDashboardAccess() และ list query ถูก scope ฝั่ง server |
| Search records | Pass | รองรับ text search ผ่าน table toolbar |
| Filter by status | Pass | UI และ API ทำงานสอดคล้องกัน |
| Filter by training type | Pass | UI และ API ทำงานสอดคล้องกัน |
| Sort records | Pass | default sort ตาม training_date desc และรองรับ server-side sort |
| Pagination | Pass | ใช้ shared DataTable pagination |
| Create training record | Pass | ใช้ TanStack Form, validation, mutation, และ certificate upload ต่อเนื่อง |
| Edit pending record | Partial | server guard ถูกต้อง แต่ client affordance กว้างกว่า permission บางกรณี |
| Delete pending record | Partial | server guard ถูกต้อง แต่ client affordance กว้างกว่า permission บางกรณี |
| View detail vs update flow | Partial | action Detail และ Update พาไป route เดียวกัน |
| Historical filtering by year | Partial | backend รองรับ year filter แต่หน้า employee training history ยังไม่ expose |
| Loading state | Pass | มี loading.tsx ทั้ง list และ detail |
| Error state | Partial | list/detail route ไม่มี dedicated error boundary และ review error page แสดง raw error |
| Empty state | Partial | มี empty state แต่ยังไม่อธิบายกรณี employee profile ไม่ถูกผูกกับ user |
Business Rule Review
สิ่งที่สอดคล้อง:
- employee เห็นเฉพาะ record ของตนเองผ่าน
buildTrainingRecordAccessClause()และgetEmployeeIdsForUser() - HRD/admin เห็นข้อมูลได้ทั้ง organization
- employee แก้ไขหรือลบได้เฉพาะ record ที่ยัง
pendingตาม server rulecanEmployeeManagePendingRecord() - duration ถูก normalize และ serialize สม่ำเสมอผ่าน minute-based helpers
จุดที่ควรระวัง:
- business rule เรื่องการแยกประวัติเป็นรายปีมี support ฝั่ง API (
year) แต่หน้า employee training history ยังไม่เปิดให้ใช้จาก UI - หาก record เป็น
pendingแต่ไม่มีuser_idผูกกับผู้ใช้งานปัจจุบัน UI ยังดูเหมือนแก้ไขได้ ทั้งที่ API จะปฏิเสธ
Role & Permission Review
Employee
- View own list: Pass
- View own detail: Pass
- Create own record: Pass
- Edit own pending record: Partial
- Delete own pending record: Partial
- Review records: Guarded
- View other employees: Guarded server-side
HRD / Admin
- View organization-scoped training records: Pass
- Review records: Pass
- Create/update/delete records: Pass ตาม role model ปัจจุบัน
ข้อสังเกต:
- ฝั่ง server permission ค่อนข้างแข็งแรง
- ฝั่ง UI มี permission drift บางจุด เพราะ action menu และ form ใช้เงื่อนไข
approval_status === 'pending'เป็นหลัก มากกว่าการยืนยัน ownership แบบเดียวกับ API
UI Review
จุดที่ดี:
- ใช้ shared DataTable stack สม่ำเสมอ
- list page responsive ในระดับดี และจำกัด overflow ไว้ใน table shell
- loading skeleton ของ list/detail ชัดเจนและสอดคล้องกับหน้า dashboard อื่น
จุดที่ควรปรับ:
- action
DetailกับUpdateใช้ปลายทางเดียวกัน ทำให้ label กับพฤติกรรมไม่ตรงกัน - empty state เป็น generic "ยังไม่มีประวัติ" โดยไม่แยกกรณี "ยังไม่ผูก employee profile"
- detail route แสดงเป็นฟอร์มเต็มทันที แม้ผู้ใช้เพียงต้องการอ่านข้อมูล
UX Review
จุดที่ดี:
- flow สร้าง record ใหม่ตรงไปตรงมา
- หลัง create/update มี toast และ redirect กลับ list
- search/filter/sort อยู่ใน pattern เดียวกับ feature อื่น ทำให้คาดเดาได้
จุดที่ควรปรับ:
- ผู้ใช้บางรายอาจกด
UpdateหรือDeleteได้จาก menu แต่ไปชน 403 ตอนเรียก API จริง - ไม่มีความต่างชัดเจนระหว่าง "ดูรายละเอียด" กับ "แก้ไข"
- หน้า list ยังขาด filter รายปี ทั้งที่บริบทธุรกิจของ training history มักอิงปีปฏิทิน
Accessibility Review
จุดที่ดี:
- action menu trigger มี
sr-onlylabel - ฟิลด์หลักในฟอร์มมี label และ
aria-invalid - ปุ่มหลักและ loading skeleton อยู่ในโครงสร้างที่อ่านได้
จุดที่ควรระวัง:
- ความต่างระหว่าง badge status ยังพึ่งข้อความมากกว่า visual emphasis
- การใช้ route เดียวกันสำหรับ detail/edit อาจทำให้ mental model ของผู้ใช้และ assistive flow สับสน
Code Quality Review
จุดที่ดี:
- แยก
types.ts,service.ts,queries.ts,mutations.ts,serverhelpers ชัดเจน - route handlers บางและส่งงาน query logic ไปที่ feature server helper
- ใช้ shared form/table stack ตาม guideline
จุดที่ควรสังเกต:
- permission logic ฝั่ง client ซ้ำแต่ไม่เทียบเท่ากับ server rule
- naming ของ detail/update flow ยังไม่สะท้อน behavior จริง
API Review
จุดที่ดี:
- contracts ของ list/detail ชัด
- route handlers ใช้
requireOrganizationAccess()และคืนค่า error ที่สม่ำเสมอพอสมควร - list endpoint รองรับ filter มากกว่าที่ UI ปัจจุบันใช้
จุดที่ควรปรับ:
- client surface ยังใช้ filter contract ไม่ครบ โดยเฉพาะ
year - review error UI แสดง
error.messageตรง ๆ ต่อผู้ใช้
Performance Review
จุดที่ดี:
- list page ใช้ server prefetch + hydration
- table ใช้ server-side pagination/sorting/filtering
- detail page prefetch เฉพาะ query ที่เกี่ยวข้อง
จุดที่ควรระวัง:
- list query สร้าง certificate preview map ให้ทุก row ซึ่งสมเหตุผลกับคอลัมน์ปัจจุบัน แต่ถ้าอนาคตมี lightweight employee view แยกจาก management view อาจแยก payload ได้อีก
Security Review
จุดที่ดี:
- page ใช้ route guard ก่อน render
- API ใช้ organization access check และ server-side record scoping
- update/delete บังคับ pending-only และ ownership เฉพาะ employee
ความเสี่ยง:
- review error page แสดง raw runtime error ต่อผู้ใช้
Consistency Review
สอดคล้อง:
- ใช้
PageContainer - ใช้ shared DataTable stack
- ใช้ TanStack Form + project wrappers
- ใช้ route handlers + Drizzle ตาม pattern กลาง
ไม่สอดคล้อง:
- detail page ของ training history ยังไม่แยก read-only view ออกจาก edit semantics
- UI permission checks ฝั่ง client ยังไม่ align กับ server helper เดียวกัน
Edge Case Review
- New employee with no history: รองรับได้ แต่ empty state ยัง generic
- Employee without linked profile: create flow block ได้ แต่ list/empty state ไม่อธิบายสาเหตุ
- Pending record without linked
user_id: มีความเสี่ยงด้าน UX เพราะ UI อาจเปิด action แต่ API ปฏิเสธ - Approved record: server กัน edit/delete ถูกต้อง
- No certificate: รองรับได้
- API failure: review route มี error UI แต่เปิด raw error; list/detail ไม่มี dedicated boundary
- Slow network: loading skeleton มีให้ทั้ง list และ detail
Problems Found
High
- Description: Client-side action และ form affordance อนุญาตให้ employee เห็น
Update/Deleteจากเงื่อนไขapproval_status === 'pending'แม้ API จะอนุญาตเฉพาะ record ที่recordUserId === actorUserId - File:
src/features/training-records/components/training-record-tables/cell-action.tsx,src/features/training-records/components/training-record-form.tsx,src/features/training-records/server/training-record-data.ts,src/app/api/training-records/[id]/route.ts - Component:
CellAction,TrainingRecordForm,canEmployeeManagePendingRecord,PATCH/DELETE /api/training-records/[id] - Severity: High
- Impact: ผู้ใช้สามารถเข้าสู่ flow แก้ไข/ลบได้ แต่ลงท้ายด้วย 403 เมื่อ record เป็น pending ที่ยังไม่ผูก
user_idอย่างครบถ้วน ทำให้ permission model ใน UI กับ backend ไม่ตรงกัน - Recommendation: ให้ client reuse permission contract เดียวกับ server หรือส่ง capability flag จาก API เพื่อไม่ให้ action menu/submit state over-promise
Medium
-
Description: หน้า employee training history ยังไม่ expose
yearfilter ทั้งที่ API, types, และ server helper รองรับแล้ว -
File:
src/app/dashboard/training-records/page.tsx,src/features/training-records/components/training-record-listing.tsx,src/features/training-records/components/training-record-tables/index.tsx,src/features/training-records/api/types.ts,src/app/api/training-records/route.ts -
Component: page route, listing page,
TrainingRecordsTable, route handler contract -
Severity: Medium
-
Impact: ผู้ใช้ยังแยกดูประวัติรายปีจากหน้า training history ไม่ได้ ทั้งที่เป็น use case ธุรกิจหลักของประวัติการอบรม
-
Recommendation: เปิด filter รายปีใน UI และเชื่อมกับ query state เดิมของหน้า
-
Description:
DetailและUpdateaction พาไป route เดียวกัน และ route นั้น render ฟอร์มแก้ไข/อ่านแบบเดียวกัน -
File:
src/features/training-records/components/training-record-tables/cell-action.tsx,src/features/training-records/components/training-record-view-page.tsx -
Component:
CellAction,TrainingRecordViewPage -
Severity: Medium
-
Impact: semantics ของหน้ารายละเอียดไม่ชัด ผู้ใช้ไม่สามารถแยกได้ว่าเข้ามาเพื่ออ่านหรือเพื่อแก้ไข และ flow นี้ทำให้ UX ของ training history ดูคลุมเครือ
-
Recommendation: แยก read-only detail state ให้ชัด หรือปรับ label/action ให้ตรงพฤติกรรมจริง
-
Description: list/detail route ของ training history ไม่มี dedicated error boundary ขณะที่ review error page แสดง raw
error.message -
File:
src/app/dashboard/training-records,src/app/dashboard/training-records/[trainingRecordId]/review/error.tsx -
Component: training history route,
TrainingRecordReviewError -
Severity: Medium
-
Impact: เมื่อ query/render พัง ผู้ใช้ได้ UX ที่ไม่สม่ำเสมอ และบางกรณีอาจเห็นข้อความเทคนิคจาก runtime
-
Recommendation: เพิ่ม route-level error UI ที่ปลอดภัยและใช้ข้อความสำหรับผู้ใช้ปลายทาง
-
Description: empty state ของหน้า list ยังไม่แยกกรณี "ไม่มีประวัติ" ออกจาก "บัญชียังไม่ถูกผูกกับ employee profile"
-
File:
src/features/training-records/components/training-record-tables/index.tsx,src/features/training-records/components/training-record-form.tsx -
Component:
TrainingRecordsTable,TrainingRecordForm -
Severity: Medium
-
Impact: ผู้ใช้และทีม support แยกสาเหตุของข้อมูลว่างได้ยาก
-
Recommendation: เพิ่ม explanatory state หรือ guidance สำหรับบัญชีที่ยังไม่ผูก employee profile
Low
- Description: status badge ใน table ใช้
outlineเหมือนกันทุกสถานะ - File:
src/features/training-records/components/training-record-tables/columns.tsx - Component:
columns - Severity: Low
- Impact: การสแกนสถานะด้วยสายตาทำได้ช้ากว่าที่ควร
- Recommendation: เพิ่ม visual distinction ระหว่าง
pending,approved,rejected,needs_revision
Missing Features
- filter รายปีสำหรับ employee training history
- dedicated error UI สำหรับ list/detail route
- explanatory empty state สำหรับบัญชีที่ยังไม่ผูก employee profile
- clear separation ระหว่าง read-only detail กับ edit flow
Improvement Opportunities
- ส่ง permission/capability flags จาก API เพื่อให้ UI action menu ตรงกับ server policy เสมอ
- แยก employee-facing training history semantics ออกจาก management/edit semantics ให้ชัดขึ้น
- พิจารณาเพิ่ม lightweight read-only summary row/detail state สำหรับ employee
- ปรับ labels และ CTA ให้สะท้อน flow จริง เช่น detail, edit, review
Final Assessment
Architecture: 8.2/10
Functionality: 7.4/10
UI: 7.2/10
UX: 6.9/10
Accessibility: 7.0/10
Performance: 7.8/10
Security: 8.0/10
Maintainability: 7.5/10
Overall Score: 7.5/10