# 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