commit
This commit is contained in:
318
docs/review/employee-training-history-audit.md
Normal file
318
docs/review/employee-training-history-audit.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Employee Training History Audit Report
|
||||
|
||||
## Summary
|
||||
|
||||
Overall Status
|
||||
|
||||
- Good
|
||||
|
||||
ภาพรวม:
|
||||
|
||||
- หน้า `Training Records` เดินตามโครงสร้างหลักของโปรเจ็กต์ค่อนข้างดี โดยใช้ `PageContainer`, shared `DataTable`, 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 rule `canEmployeeManagePendingRecord()`
|
||||
- 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-only` label
|
||||
- ฟิลด์หลักในฟอร์มมี 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`, `server` helpers ชัดเจน
|
||||
- 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 `year` filter ทั้งที่ 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` และ `Update` action พาไป 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
|
||||
Reference in New Issue
Block a user