298 lines
16 KiB
Markdown
298 lines
16 KiB
Markdown
# 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
|