# 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