This commit is contained in:
2026-07-16 09:53:14 +07:00
parent 0fc112a2e8
commit 29cec708a3
1236 changed files with 212848 additions and 0 deletions

View File

@@ -0,0 +1,297 @@
# Pending Review Audit Report
## Summary
Overall Status
- Good
ภาพรวม:
- หน้า `Pending Review` มีโครงสร้างค่อนข้างดี ใช้ shared DataTable stack, React Query, route handler, server-side guard และ validation ตาม pattern ของโปรเจกต์
- มี loading state, error state, empty state, pagination, search, sorting, filtering และ review flow ครบในระดับใช้งานจริง
- อย่างไรก็ตาม ยังมีจุดสำคัญที่ทำให้ semantics ของหน้าไม่คมพอ โดยเฉพาะเรื่องหน้า `Pending Review` ยังไม่ได้บังคับให้เป็น “pending only” จริงทั้ง flow
---
## Architecture Review
สิ่งที่พบ:
- Route หลักใช้ [src/app/dashboard/pending-review/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/pending-review/page.tsx)
- หน้า list ใช้ server prefetch ผ่าน [src/features/training-records/components/pending-review-listing.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-listing.tsx)
- table ใช้ shared `DataTable`, `DataTableToolbar`, `useDataTable`
- data source ใช้ feature เดียวกับ `training-records` ผ่าน `trainingRecordsQueryOptions()`
- review action ใช้ dedicated route `/dashboard/training-records/[trainingRecordId]/review`
- API ใช้ route handler และ Drizzle
- ไม่พบ Prisma หรือ Server Actions ใน flow นี้
ข้อสรุป:
- สถาปัตยกรรมโดยรวมสอดคล้องกับมาตรฐานโปรเจกต์
- จุดที่ยังไม่คมคือหน้า `pending-review` ยัง reuse list endpoint แบบ generic มากกว่าจะมี scope ที่ชัดว่าเป็น review queue โดยตรง
---
## UI Review
จุดที่ดี:
- ใช้ `PageContainer` ถูกต้อง
- ปุ่ม action หน้า page header และปุ่มในฟอร์มมี responsive behavior พอใช้
- ตารางใช้ shell กลางของโปรเจกต์ ทำให้ spacing และ pagination ค่อนข้างสม่ำเสมอ
- action menu ในตารางมีขนาดพอเหมาะและมี `aria-label`
จุดที่ควรระวัง:
- filter `company`, `department`, `year` เป็น text input ทั้งหมด ทำให้ UX ไม่แน่นเท่า faceted/select filter
- badge status ใช้ `variant='outline'` เหมือนกันทุกสถานะ ทำให้ความแตกต่างทางสายตาระหว่าง `pending`, `approved`, `rejected`, `needs_revision` ไม่ชัด
- empty state ไม่มีทางลัดให้ล้าง filter จากหน้าจอเดียว
---
## UX Review
จุดที่ดี:
- หากไม่มี `status` ใน URL จะ redirect ไป `?status=pending`
- มีเส้นทางกลับไปหน้ารายการอบรมหลัก
- review form หลัง submit สำเร็จจะ toast, redirect กลับ queue, และ refresh ข้อมูล
จุดที่ควรปรับ:
- ผู้ใช้ยังสามารถเปลี่ยน filter status ไปดู `approved`, `rejected`, `needs_revision` บนหน้า `Pending Review` ได้ ทำให้ชื่อหน้ากับพฤติกรรมจริงไม่ตรงกัน
- เมื่อกรองแล้วไม่เจอข้อมูล ผู้ใช้ไม่เห็น toolbar และไม่เห็นปุ่ม reset filter
- error boundary แสดง `error.message` ตรง ๆ ซึ่งอาจไม่เหมาะกับผู้ใช้ปลายทาง
---
## Permission Review
### Employee
Can View:
- ไม่ควรเข้าหน้า `Pending Review`
- ไม่ควรเข้าหน้า review รายการ
- ไม่ควรเรียก review API
Can Edit:
- ไม่มีสิทธิ์
Can Delete:
- ไม่มีสิทธิ์
หลักฐาน:
- หน้า `Pending Review` ใช้ `requireHRDDashboardAccess()`
- review API เช็ก `canReviewTrainingRecords(membership.role)`
### HRD
Can View:
- เข้าหน้า `Pending Review`
- เข้าหน้า review รายการ
- ดูรายการใน queue ได้ทั้งองค์กร
Can Edit:
- อนุมัติ / ไม่อนุมัติรายการที่ยัง `pending`
Can Delete:
- ไม่มี delete action ในหน้านี้
### Admin IT / Super Admin
Can View:
- เข้าหน้า `Pending Review` ได้ผ่าน business role mapping
- เข้าหน้า review และ review API ได้ ถ้ามี organization membership ในบริบทนั้น
Can Edit:
- อนุมัติ / ไม่อนุมัติได้เหมือน HRD
Can Delete:
- ไม่มี delete action ในหน้านี้
ข้อสังเกต:
- สิทธิ์ review จริงที่ API อิงกับ `membership.role === owner/admin` ไม่ได้อิงชื่อ business role โดยตรง
- ในทางปฏิบัติถือว่า HRD และ IT ใช้งานได้ แต่ logic สิทธิ์ยังเป็น “ตีความร่วม” มากกว่า matrix ที่ชัดเจน
---
## Functional Review
Checklist
| Feature | Status | Notes |
| ------- | ------ | ----- |
| Display pending records | Partial | หน้า default เป็น `status=pending` แต่ผู้ใช้ยังเปลี่ยนไปดู status อื่นได้ |
| Pagination | Pass | ใช้ shared `useDataTable` + `DataTablePagination` |
| Search | Pass | ใช้ text filter ที่ column `search` |
| Sorting | Pass | รองรับ server-side sorting |
| Filtering | Pass | มี filter ตาม `status`, `company`, `department`, `year` |
| Responsive layout | Pass | table shell รองรับ overflow และปุ่มหลักไม่ล้นง่าย |
| Loading state | Pass | มี `loading.tsx` |
| Empty state | Partial | มี empty state แต่ไม่มี reset filter / toolbar ใน state นี้ |
| Error state | Partial | มี `error.tsx` แต่แสดง raw error message |
| Refresh after action | Pass | mutation invalidate query + push + refresh |
| Correct badge/status | Partial | label ถูก แต่ visual distinction ยังไม่ชัด |
| Correct action buttons | Pass | มี `ตรวจสอบ` และ `ดูรายละเอียด` |
| Correct permissions | Pass | page/API กันสิทธิ์ค่อนข้างถูกต้อง |
---
## Code Quality Review
จุดที่ดี:
- แยก route, listing, table, columns, action menu, schema, service, queries, mutations, server data helper ชัดเจน
- ใช้ shared DataTable stack ตาม guideline
- ใช้ Zod validation ใน review API
- ใช้ React Query cache invalidation ตาม feature key
- ไม่มีการ import mock data
จุดที่ควรสังเกต:
- หน้า `Pending Review` reuse generic `training-records` list แทนที่จะมี query helper ที่สื่อชัดว่าเป็น review queue
- `TrainingRecordReviewDialog` ยังอยู่ใน feature แต่ไม่ได้เป็น flow หลักของหน้า review ปัจจุบัน ดูเหมือนมีโอกาสเป็น dead/legacy path
- การใช้ generic endpoint ทำให้ semantics ของหน้า review queue พึ่งพา URL filter มากเกินไป
---
## Security Review
จุดที่ดี:
- หน้า `Pending Review` กัน direct URL ด้วย `requireHRDDashboardAccess()`
- review page กัน direct URL ด้วย `requireHRDDashboardAccess()`
- review API ใช้ server-side auth ผ่าน `requireOrganizationAccess()`
- review API เช็กสิทธิ์ซ้ำผ่าน `canReviewTrainingRecords()`
- review API ป้องกันการ review ซ้ำโดยเช็กว่า record ต้องอยู่สถานะ `pending`
- data list ถูก scope ด้วย organization และ membership role
ความเสี่ยง:
- `error.tsx` แสดง `error.message` ตรง ๆ ซึ่งอาจเผยรายละเอียดภายใน เช่นข้อความจาก backend หรือฐานข้อมูล
---
## Performance Review
จุดที่ดี:
- ใช้ server prefetch + hydration
- ใช้ manual pagination / filtering / sorting ที่สอดคล้องกับ server-side data source
- query invalidation หลัง review ทำครบทั้ง list และ detail key
- ไม่พบ N+1 query ที่แย่ในระดับ row-by-row สำหรับตารางนี้
จุดที่ควรปรับ:
- หน้า `pending-review` ใช้ generic list query ที่โหลด certificate preview map สำหรับทุก row แม้ตารางนี้ไม่ได้แสดง certificate preview โดยตรง
- ถ้าหน้านี้มีการใช้งานบ่อยและข้อมูลเยอะ อาจแยก endpoint/query ของ review queue โดยตัด field ที่ไม่จำเป็นออกได้
---
## Problems Found
### High
- Description: หน้า `Pending Review` ไม่ได้บังคับให้แสดงเฉพาะรายการ `pending` จริง
- File: [page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/pending-review/page.tsx:24), [pending-review-table.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-table.tsx:35), [pending-review-columns.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-columns.tsx:117)
- Component: `Page`, `PendingReviewTable`, `pendingReviewColumns`
- Reason: หน้า redirect แค่กรณีไม่มี `status` แต่ยังเปิดให้ filter เป็น `approved`, `rejected`, `needs_revision`
- Impact: ชื่อหน้าและความหมายของ queue ไม่ตรงกับพฤติกรรมจริง ทำให้ผู้ใช้สับสน และทำให้ขอบเขตของ review queue ไม่ชัด
### Medium
- Description: เมื่อกรองแล้วไม่เจอข้อมูล ระบบซ่อน toolbar ทั้งก้อน ทำให้ผู้ใช้ไม่มีปุ่มล้าง filter จากหน้าจอเดียว
- File: [pending-review-table.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-table.tsx:60)
- Component: `PendingReviewTable`
- Reason: empty state return ก่อน render `DataTableToolbar`
- Impact: UX ติดขัด โดยเฉพาะเมื่อ user search/filter พลาดแล้วไม่รู้จะ reset อย่างไร
- Description: error boundary แสดง raw `error.message` ให้ผู้ใช้
- File: [error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/pending-review/error.tsx:17)
- Component: `PendingReviewError`
- Reason: render ข้อความ error จาก runtime ตรง ๆ
- Impact: เสี่ยงเผยข้อความภายในระบบ และทำให้ UX ดู technical เกินไป
- Description: หน้า queue ใช้ endpoint generic ของ training records แทน query ที่สื่อเฉพาะ review queue
- File: [pending-review-listing.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-listing.tsx:22), [training-record-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/server/training-record-data.ts:245)
- Component: `PendingReviewListingPage`, `listTrainingRecordsForOrganization`
- Reason: reuse ได้ แต่ semantics ของ feature ไม่ชัด
- Impact: maintainability ลดลง และทำให้หน้า queue รับ status/filter เกินขอบเขตหน้าที่
### Low
- Description: badge status ใช้รูปแบบ visual เดียวกันทุกสถานะ
- File: [pending-review-columns.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-columns.tsx:107)
- Component: `pendingReviewColumns`
- Reason: ใช้ `Badge variant="outline"` ทุกค่า
- Impact: สแกนสถานะได้ช้ากว่าที่ควร
- Description: filter `year`, `company`, `department` ยังเป็น text-based ทั้งหมด
- File: [pending-review-columns.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/pending-review-columns.tsx:45)
- Component: `pendingReviewColumns`
- Reason: ใช้งานได้ แต่ UX ไม่แข็งแรงเท่า select/faceted options
- Impact: ใช้งานได้แต่ความเร็วในการกรองต่ำลง โดยเฉพาะกับผู้ใช้ธุรการ
- Description: `TrainingRecordReviewDialog` ดูเป็น flow เก่าที่ไม่ได้เป็น entry point หลักของ pending review ปัจจุบัน
- File: [training-record-review-dialog.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/training-records/components/training-record-review-dialog.tsx:1)
- Component: `TrainingRecordReviewDialog`
- Reason: หน้าปัจจุบันพาไป review page เต็มแทน dialog
- Impact: เพิ่มภาระในการดูแลโค้ดและทำให้ feature surface ดูซ้ำซ้อน
---
## Recommendations
- บังคับให้หน้า `Pending Review` เป็น `pending-only` จริงทั้ง route, filter UI, และ query contract
- ใน empty state ควรยังแสดง toolbar หรืออย่างน้อยมีปุ่ม `ล้างตัวกรอง`
- เปลี่ยน error message สำหรับผู้ใช้ให้เป็นข้อความที่ปลอดภัยและอ่านง่าย แล้ว log รายละเอียดไว้ภายในแทน
- ถ้าหน้า review queue เป็นฟีเจอร์สำคัญ ควรแยก query/helper สำหรับ review queue โดยเฉพาะ เพื่อให้ field และ logic ชัดกว่าการ reuse list generic
- เพิ่ม visual distinction ของ status badge
- พิจารณาปรับ filter บางตัวเป็น faceted/select หากมี option data พร้อมใช้อยู่แล้ว
- ตรวจสอบว่า `TrainingRecordReviewDialog` ยังจำเป็นหรือไม่ และถ้าไม่ใช้จริงควรวางแผน cleanup ในรอบถัดไป
---
## Missing Features
- การบังคับ queue ให้เป็น `pending-only` แบบชัดเจน
- ปุ่ม reset filter ใน empty state
- visual emphasis ของแต่ละ status
- query contract เฉพาะสำหรับ review queue
---
## Final Score
Architecture:
8/10
UI:
7/10
UX:
6.5/10
Performance:
7.5/10
Security:
8/10
Maintainability:
7.5/10
Overall:
7.4/10