commit
This commit is contained in:
336
docs/review/employee-create-training-record-audit.md
Normal file
336
docs/review/employee-create-training-record-audit.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# Employee Create Training Record Audit Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Overall Status:
|
||||
|
||||
- Needs Improvement
|
||||
|
||||
ภาพรวม:
|
||||
|
||||
- flow สร้างประวัติการอบรมของ employee ใช้โครงสร้างหลักของโปรเจ็กต์ได้ดี ทั้ง route guard, shared form stack, minute-based duration input, local route handlers, และ server-side ownership checks
|
||||
- อย่างไรก็ดี implementation ปัจจุบันรองรับเพียงชุดข้อมูลพื้นฐานคือ employee, course, date, training type, duration, organizer, note, และ certificate upload หลัง create โดยยังไม่มีหลาย capability ที่ plan นี้คาดหวัง เช่น `location type`, `online URL`, `duplicate warning`, และ single-file certificate rule
|
||||
- จุดเสี่ยงหลักอยู่ที่ช่องว่างระหว่าง business requirement ในแผนกับ behavior จริงของหน้า create รวมถึงการ upload หลายไฟล์, การไม่มี duplicate warning, และ validation ที่ยังไม่ครอบคลุมบางกติกาเชิงธุรกิจ
|
||||
|
||||
---
|
||||
|
||||
## Scope Reviewed
|
||||
|
||||
- Route/page:
|
||||
- `src/app/dashboard/training-records/page.tsx`
|
||||
- `src/app/dashboard/training-records/[trainingRecordId]/page.tsx`
|
||||
- `src/lib/auth/page-guards.ts`
|
||||
- Form/components:
|
||||
- `src/features/training-records/components/training-record-form.tsx`
|
||||
- `src/features/training-records/components/training-record-view-page.tsx`
|
||||
- `src/features/training-records/components/training-record-course-combobox.tsx`
|
||||
- Validation/types/query/mutation:
|
||||
- `src/features/training-records/schemas/training-record.ts`
|
||||
- `src/features/training-records/api/types.ts`
|
||||
- `src/features/training-records/api/service.ts`
|
||||
- `src/features/training-records/api/queries.ts`
|
||||
- `src/features/training-records/api/mutations.ts`
|
||||
- `src/features/training-records/api/certificate-mutations.ts`
|
||||
- API/server/security:
|
||||
- `src/app/api/training-records/route.ts`
|
||||
- `src/app/api/training-records/[id]/certificates/route.ts`
|
||||
- `src/features/training-records/server/training-record-data.ts`
|
||||
- `src/lib/certificate-storage.ts`
|
||||
- Navigation:
|
||||
- `src/config/nav-config.ts`
|
||||
|
||||
---
|
||||
|
||||
## Functional Review
|
||||
|
||||
| Feature | Status | Notes |
|
||||
| ------- | ------ | ----- |
|
||||
| Employee can open create page | Pass | ใช้ dynamic route `[trainingRecordId]` ค่า `new` ผ่าน `TrainingRecordViewPage` |
|
||||
| Employee can create own training record | Pass | form บังคับ employee ID จาก current user และ API เช็ก ownership ฝั่ง server |
|
||||
| Employee cannot create for another employee | Pass | UI ซ่อน employee selector สำหรับ employee และ API เช็ก `assertUserAndCourseAccess()` |
|
||||
| Course selection / custom course | Pass | รองรับเลือก course master หรือกรอกชื่อ course เอง |
|
||||
| Training date required | Pass | มี client schema validation |
|
||||
| Training hours hour/minute format | Pass | ใช้ `DurationPicker` และ validate 1-1800 นาที |
|
||||
| Training type required | Pass | มี select และ schema validation |
|
||||
| Provider / institute | Pass | ใช้ field `organizer` แบบ optional |
|
||||
| Note | Pass | รองรับเป็น optional textarea |
|
||||
| Upload certificate/evidence | Partial | รองรับ upload หลัง create แต่เป็น multi-file ไม่ใช่ single-file |
|
||||
| Submit loading state | Pass | ปุ่ม submit disabled ระหว่าง create/update/upload |
|
||||
| Submit success feedback | Pass | มี toast success และ redirect กลับ history |
|
||||
| Submit error feedback | Partial | มี toast จาก error.message แต่ยังไม่คัดข้อความให้เหมาะกับผู้ใช้เสมอ |
|
||||
| Redirect after submit | Pass | `router.push('/dashboard/training-records')` หลัง create สำเร็จ |
|
||||
| Created record status is pending | Pass | API set `approvalStatus: 'pending'` ตายตัว |
|
||||
| Employee cannot set K/S/A category | Pass | ไม่มี field ใน form/payload และ API ไม่รับจาก client |
|
||||
| Employee cannot set approval status | Pass | ไม่มี field ใน form/payload และ API set เอง |
|
||||
| Location type | Fail | ไม่มี field หรือ contract นี้ใน implementation |
|
||||
| Online URL | Fail | ไม่มี field หรือ validation นี้ใน implementation |
|
||||
| Duplicate training warning | Fail | ไม่พบ duplicate-check หรือ warning flow |
|
||||
|
||||
---
|
||||
|
||||
## Business Rule Review
|
||||
|
||||
| Rule | Status | Notes |
|
||||
| ---- | ------ | ----- |
|
||||
| Employee submits training records for themselves only | Pass | server ownership check ผ่าน `assertUserAndCourseAccess()` |
|
||||
| Employee cannot edit HRD-only fields | Pass | form create ไม่มี category/status/review fields |
|
||||
| HRD assigns K/S/A later | Pass | category ถูกจัดการใน review flow เท่านั้น |
|
||||
| HRD verifies and approves later | Pass | create route บันทึกสถานะ `pending` เสมอ |
|
||||
| Employee cannot approve own record | Pass | create flow ไม่มี approval capability |
|
||||
| Employee cannot change approval status | Pass | API ไม่รับ status จาก client |
|
||||
| Calendar year handled correctly | Partial | รับและบันทึก date ได้ แต่ไม่มี create-side business messaging เรื่องปี |
|
||||
| Training hours support hours and minutes | Pass | minute-based input และ normalize เป็น decimal hours ก่อนบันทึก |
|
||||
| Certificate attachment allows 1 file only | Fail | UI และ API รองรับหลายไฟล์ (`maxFiles=5`, `multiple`) |
|
||||
| Certificate/evidence rules match project requirements | Partial | file type/size validation มี แต่จำนวนไฟล์ไม่ตรงกับ plan |
|
||||
| Online training URL rules are consistent | Fail | ไม่มี field/validation นี้ใน create flow |
|
||||
| Duplicate course behavior is warn but allow save | Fail | ไม่พบ logic เตือน duplicate |
|
||||
| New record should appear in Pending Review / History correctly | Pass | create mutation invalidate `trainingRecordKeys.all` และ API บันทึกสถานะ pending |
|
||||
|
||||
---
|
||||
|
||||
## Role & Permission Review
|
||||
|
||||
| Permission | Status | Notes |
|
||||
| ---------- | ------ | ----- |
|
||||
| Employee can open create training record page | Pass | `requireEmployeeDashboardAccess()` อนุญาต employee area |
|
||||
| Employee can create own training record | Pass | current user employeeId ถูกใช้เป็น payload สำหรับ employee |
|
||||
| Employee can upload own certificate/evidence | Pass | upload route อนุญาตเฉพาะ owner ของ pending record |
|
||||
| Employee can cancel and return | Pass | ปุ่มกลับใช้ `router.back()` |
|
||||
| Employee cannot create for another employee via UI | Pass | ไม่มี employee selector สำหรับ employee |
|
||||
| Employee cannot create for another employee via API | Pass | `assertUserAndCourseAccess()` บังคับ ownership server-side |
|
||||
| Employee cannot assign K/S/A | Pass | ไม่มี field และ API ไม่รับ |
|
||||
| Employee cannot approve/reject | Pass | create route set pending ตายตัว |
|
||||
| Employee cannot change status manually | Pass | payload ไม่มี field status |
|
||||
| Employee cannot bypass file validation through direct API call | Partial | type/size validation มี แต่จำนวนไฟล์หลายไฟล์ยังผ่าน |
|
||||
| Employee cannot access HRD-only create/edit pages | Partial | หน้าเดียวกันถูก reuse สำหรับ create/edit; protection หลักอยู่ที่ data access และ form state มากกว่าแยก route semantics |
|
||||
|
||||
---
|
||||
|
||||
## UI Review
|
||||
|
||||
- layout ของ form ใช้ `Card`, shared inputs, shared select, และ spacing สอดคล้องกับ feature อื่นในโปรเจ็กต์
|
||||
- required fields ชัดสำหรับ course, date, type, duration แต่ optional/required semantics ของ `organizer` ยังไม่ได้สะท้อนกติกาธุรกิจพิเศษใด ๆ
|
||||
- `DurationPicker` ทำให้กรอกชั่วโมง/นาทีได้ชัดกว่าช่องตัวเลขดิบ
|
||||
- upload section อธิบายชนิดไฟล์ชัด แต่สื่อสารเป็น “แนบเอกสารได้หลายไฟล์” ผ่าน UI จริง แม้ requirement ใน plan ระบุ single-file
|
||||
- ไม่มี field group สำหรับ location mode, online URL, หรือ duplicate warning section ทำให้หน้า create ปัจจุบันดูเล็กกว่าขอบเขตธุรกิจที่แผนคาดหวัง
|
||||
- ปุ่ม action บน mobile ใช้ full-width ตาม pattern ที่ดีของ repo
|
||||
|
||||
---
|
||||
|
||||
## UX Review
|
||||
|
||||
- flow พื้นฐานเข้าใจง่าย: เลือกหลักสูตร, เลือกวัน, เลือกประเภท, ระบุระยะเวลา, แนบเอกสาร, ส่ง
|
||||
- สำหรับ employee ทั่วไป การ fix employee ให้เป็น current user ช่วยลดโอกาสกรอกผิดคน
|
||||
- อย่างไรก็ดี requirement เชิง domain บางอย่างหายไปจาก UX ทั้งหมด เช่น duplicate warning, online URL guidance, และ location-specific branching
|
||||
- upload ถูกเลื่อนไปหลัง create ซึ่งใช้งานได้ แต่ทำให้ submit result ขึ้นกับ 2 mutations ต่อเนื่อง หาก upload ล้มเหลวหลัง create สำเร็จ ผู้ใช้จะได้ record ถูกสร้างแต่ attachment ไม่ครบ
|
||||
- ปุ่มกลับใช้ `router.back()` ซึ่งอาจพาผู้ใช้กลับไปหน้าที่ไม่คาดหวัง หากเข้าฟอร์มจาก deep link แทนที่จะเข้าจาก history list
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Review
|
||||
|
||||
- form fields ส่วนใหญ่มี label และ `aria-invalid`
|
||||
- upload ใช้ shared `FileUploader` แต่จากหน้า create นี้ยังไม่เห็นข้อความผูกกับ error state ของ uploader โดยตรง
|
||||
- textarea note ใช้ `aria-label`
|
||||
- ปุ่ม action เป็นข้อความปกติ อ่านได้ง่าย
|
||||
- ยังไม่มี evidence ว่ามี accessible description เฉพาะสำหรับ upload constraints เช่น “ได้ 1 ไฟล์เท่านั้น” เพราะ implementation จริงไม่ใช้กติกานั้น
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Review
|
||||
|
||||
- โครงสร้างโดยรวมดี: page -> feature component -> mutation/service -> route handler -> server helper
|
||||
- form component ค่อนข้างใหญ่และรวม create/edit/upload behavior ไว้ในไฟล์เดียว ทำให้ความรับผิดชอบหลายส่วนอยู่ร่วมกัน
|
||||
- validation schema กับ form fields ยังสอดคล้องกันดีในสิ่งที่ implementation รองรับจริง
|
||||
- มี import helper เวลา เช่น `formatDurationHHMM` และ `formatDurationThai` ใน form แต่ไม่ได้ถูกใช้งานในหน้า create นี้
|
||||
- ไม่มีร่องรอย duplicate-check abstraction หรือ online/location-specific contracts ใน feature นี้
|
||||
|
||||
---
|
||||
|
||||
## API Review
|
||||
|
||||
- create mutation เรียก `POST /api/training-records` ผ่าน `apiClient`
|
||||
- API route ใช้ `requireOrganizationAccess()` และเช็ก ownership ผ่าน `assertUserAndCourseAccess()`
|
||||
- API set `approvalStatus: 'pending'` ฝั่ง server เสมอ
|
||||
- payload ของ create จำกัดอยู่ที่ `employeeId`, `courseId/courseName`, `trainingDate`, `trainingType`, `submittedMinutes`, `organizer`, `note`
|
||||
- upload certificates ใช้ route แยกหลัง create สำเร็จ และ invalidate เฉพาะ certificates query key
|
||||
- ไม่พบ duplicate-check endpoint หรือ warning contract
|
||||
- ไม่พบ validation/business contract สำหรับ `online URL` หรือ `location type`
|
||||
|
||||
---
|
||||
|
||||
## Security Review
|
||||
|
||||
- route guard: employee ต้องผ่าน `requireEmployeeDashboardAccess()`
|
||||
- API guard: ต้องมี session + organization ผ่าน `requireOrganizationAccess()`
|
||||
- ownership check: employee ส่งให้คนอื่นไม่ได้ เพราะ `assertUserAndCourseAccess()` เทียบ employee IDs ของ actor กับ target
|
||||
- approval/category fields ไม่เปิดจาก client
|
||||
- file upload มี server-side type/size validation ที่ `saveCertificateFile()`
|
||||
- path traversal มีการกันผ่าน `ensureWithinUploadRoot()`
|
||||
- ช่องโหว่สำคัญเชิง requirement คือ API ยังรับหลายไฟล์ได้ แม้ plan ตั้งใจ single-file
|
||||
|
||||
---
|
||||
|
||||
## Performance Review
|
||||
|
||||
- create page render เป็น client form ปกติ ไม่พบ heavy query หลายตัวเกินจำเป็นสำหรับ employee
|
||||
- employee ไม่โหลด `userOptions` เพราะ query นี้เปิดเฉพาะ HRD
|
||||
- course combobox preload course options ทั้งชุดแรก (`limit=50`) ซึ่งพอใช้ได้ในระยะสั้น แต่ไม่มี search-as-you-type จริง
|
||||
- create flow ใช้ sequential mutation: create ก่อน แล้วค่อย upload file(s); ถ้าแนบหลายไฟล์หรือไฟล์ใหญ่ UX จะช้าลงตามจำนวนไฟล์
|
||||
|
||||
---
|
||||
|
||||
## Data Validation Review
|
||||
|
||||
- Missing course name: Pass ผ่าน schema/course selection logic
|
||||
- Missing training date: Pass
|
||||
- Missing training hours: Pass
|
||||
- Invalid hour/minute format: Pass ในกรอบของ `DurationPicker`
|
||||
- Zero hour behavior: Pass ถูก block ที่ `> 0`
|
||||
- Over 30 hours behavior: Pass ถูก block ที่ `<= 1800` นาที
|
||||
- Missing training type: Pass
|
||||
- Missing location type: Fail ไม่มี field นี้
|
||||
- Missing provider: Partial `organizer` เป็น optional
|
||||
- Invalid online URL: Fail ไม่มี field นี้
|
||||
- Missing certificate when required: Fail ไม่มี conditional rule จาก `certificate_required`
|
||||
- Multiple file upload: Fail ตาม requirement single-file แต่ implementation อนุญาตหลายไฟล์
|
||||
- Invalid file type: Pass
|
||||
- Invalid file size: Pass
|
||||
- Null handling: Pass ในระดับพื้นฐาน
|
||||
- Incorrect defaults: Partial default date ใช้ `new Date().toISOString().slice(0, 10)` ซึ่งอิง UTC มากกว่า business timezone
|
||||
|
||||
---
|
||||
|
||||
## Edge Case Review
|
||||
|
||||
- New employee: ถ้ามี employee link แล้วสร้างได้; ถ้าไม่มีจะถูก block ก่อน submit
|
||||
- Employee profile missing: มี toast block ฝั่ง client และ server จะกันต่อผ่าน ownership check
|
||||
- No course master: รองรับผ่าน custom course
|
||||
- Course name duplicate: ไม่พบ warning flow
|
||||
- Training date in future: ไม่พบ rule ป้องกัน
|
||||
- Training date in previous year: บันทึกได้ ไม่มี warning
|
||||
- `00:00` hours: ถูก block
|
||||
- `00:15` hours: รองรับ
|
||||
- `01:45` hours: รองรับ
|
||||
- `30:00` hours: รองรับ
|
||||
- More than 30 hours: ถูก block
|
||||
- Online training without URL: ไม่มี field นี้ จึงไม่ถูกบังคับ
|
||||
- Offline training with URL: ไม่มี field นี้ จึงไม่เกี่ยว
|
||||
- File upload failed: record อาจถูก create สำเร็จแต่ attachment ไม่ขึ้น เพราะ upload เป็น step ที่สอง
|
||||
- API failure: form แสดง toast จาก `error.message`
|
||||
- Slow network: ปุ่ม disabled ระหว่าง mutation แต่ไม่มี progress state รายไฟล์
|
||||
- Double submit: ปุ่ม submit ถูก disable ระหว่าง pending
|
||||
- Permission denied: server-side ownership checks มีอยู่
|
||||
|
||||
---
|
||||
|
||||
## Problems Found
|
||||
|
||||
### Critical
|
||||
|
||||
- None observed.
|
||||
|
||||
### High
|
||||
|
||||
- Description: หน้า create ไม่มี `location type`, `online URL`, และ duplicate warning ทั้งที่เป็น scope หลักตาม audit plan นี้
|
||||
- File: `src/features/training-records/components/training-record-form.tsx`, `src/features/training-records/schemas/training-record.ts`, `src/features/training-records/api/types.ts`, `src/app/api/training-records/route.ts`
|
||||
- Component: `TrainingRecordForm`, `trainingRecordSchema`, create contract
|
||||
- Severity: High
|
||||
- Impact: flow ปัจจุบันไม่ครอบคลุม requirement เชิงธุรกิจตามแผน ทำให้ user ไม่สามารถกรอก/ตรวจสอบข้อมูลสำคัญบางประเภทได้เลย
|
||||
- Recommendation: กำหนด canonical contract ของ create flow ให้ชัดก่อนว่าต้องมี field/business rule ใดบ้าง แล้วค่อยเติมทั้ง form, schema, payload, และ route handler ให้ตรงกัน
|
||||
|
||||
- Description: UI และ API ของ certificate upload อนุญาตหลายไฟล์ (`maxFiles={5}`, `multiple`, loop ผ่านทุกไฟล์) ขัดกับ requirement single-file ในแผน
|
||||
- File: `src/features/training-records/components/training-record-form.tsx:580`, `src/app/api/training-records/[id]/certificates/route.ts:106`, `src/lib/certificate-storage.ts:27`
|
||||
- Component: `FileUploader`, `POST /api/training-records/[id]/certificates`
|
||||
- Severity: High
|
||||
- Impact: ผู้ใช้สามารถแนบหลักฐานหลายไฟล์ได้จริง และข้อมูลที่บันทึกจะไม่ตรง business rule หากระบบต้องการหลักฐาน 1 ไฟล์ต่อ record
|
||||
- Recommendation: ตกลงกติกาจำนวนไฟล์ให้ชัด แล้ว align ทั้ง UI, mutation, และ API validation ไปทางเดียวกัน
|
||||
|
||||
### Medium
|
||||
|
||||
- Description: ไม่พบ duplicate-check หรือ warning flow ใด ๆ ก่อน create แม้ plan ระบุ “warn but allow save”
|
||||
- File: `src/features/training-records/components/training-record-form.tsx`, `src/features/training-records/server/training-record-data.ts`, `src/app/api/training-records/route.ts`
|
||||
- Component: create form, create API
|
||||
- Severity: Medium
|
||||
- Impact: ผู้ใช้สามารถส่งข้อมูลซ้ำโดยไม่มี feedback ทำให้เกิด duplicate history records ได้ง่าย
|
||||
- Recommendation: เพิ่ม business decision ก่อนว่า duplicate วัดจาก course/date/type/employee combination แบบใด แล้วค่อยออกแบบ warning flow
|
||||
|
||||
- Description: ไม่มี rule บังคับหรือ messaging จาก `certificate_required` แม้ course options ส่ง field นี้มาด้วย
|
||||
- File: `src/features/training-records/api/types.ts:100`, `src/features/training-records/components/training-record-course-combobox.tsx:55`, `src/features/training-records/components/training-record-form.tsx:572`
|
||||
- Component: course option contract, create form
|
||||
- Severity: Medium
|
||||
- Impact: หลักสูตรที่ควรต้องแนบ certificate อาจถูกส่งโดยไม่มีหลักฐานได้ และ UI ไม่สื่อ requirement นี้กับผู้ใช้
|
||||
- Recommendation: ตัดสินใจให้ชัดว่าต้อง enforce ตอน create เลยหรือหลัง create แล้วสื่อสารผ่าน UI และ validation ให้ตรงกัน
|
||||
|
||||
- Description: default วันที่ใช้ `new Date().toISOString().slice(0, 10)` ซึ่งอิง UTC และอาจคลาดกับวันตาม timezone ธุรกิจ
|
||||
- File: `src/features/training-records/components/training-record-form.tsx:143`
|
||||
- Component: `TrainingRecordForm`
|
||||
- Severity: Medium
|
||||
- Impact: ผู้ใช้ใกล้ช่วงเปลี่ยนวันอาจเห็นค่า default date เลื่อนไป 1 วันจากเวลาท้องถิ่น
|
||||
- Recommendation: ใช้ local-date strategy เดียวกับธุรกิจของระบบแทนค่า ISO UTC ตรง ๆ
|
||||
|
||||
- Description: upload เกิดหลัง create แบบ sequential; ถ้า upload ล้มเหลว record จะถูกสร้างแล้วแต่ attachment หาย ทำให้ flow สำเร็จเพียงบางส่วน
|
||||
- File: `src/features/training-records/components/training-record-form.tsx:222`
|
||||
- Component: `TrainingRecordForm`
|
||||
- Severity: Medium
|
||||
- Impact: user อาจคิดว่าการส่งสำเร็จครบถ้วน แต่จริง ๆ เหลือ pending record ที่ไม่มีหลักฐานแนบ
|
||||
- Recommendation: เพิ่ม UX/state ที่สื่อชัดว่า create สำเร็จแต่ upload ล้มเหลว หรือออกแบบ transactional expectation ให้ชัด
|
||||
|
||||
- Description: ปุ่มกลับใช้ `router.back()` แทน destination คงที่
|
||||
- File: `src/features/training-records/components/training-record-form.tsx:591`
|
||||
- Component: `TrainingRecordForm`
|
||||
- Severity: Medium
|
||||
- Impact: หากผู้ใช้เข้าฟอร์มจาก bookmark, deep link, หรือหน้าอื่น ปุ่มกลับอาจพาไปบริบทที่ไม่คาดหวัง
|
||||
- Recommendation: พิจารณา fallback destination ที่ชัด เช่น history list
|
||||
|
||||
### Low
|
||||
|
||||
- Description: `TrainingRecordForm` รวม create, edit, upload, review-status display ไว้ใน component เดียว ทำให้ไฟล์ค่อนข้างใหญ่
|
||||
- File: `src/features/training-records/components/training-record-form.tsx`
|
||||
- Component: `TrainingRecordForm`
|
||||
- Severity: Low
|
||||
- Impact: maintainability ลดลงเมื่อเพิ่ม business rules ใหม่
|
||||
- Recommendation: หากมีการขยาย flow เพิ่ม อาจแยก concern บางส่วนเป็น subcomponents
|
||||
|
||||
- Description: มี import helper เวลา `formatDurationHHMM` และ `formatDurationThai` ที่ไม่ถูกใช้ในหน้า create
|
||||
- File: `src/features/training-records/components/training-record-form.tsx:40`
|
||||
- Component: `TrainingRecordForm`
|
||||
- Severity: Low
|
||||
- Impact: signal ว่า component ผ่านการเปลี่ยนแปลงหลายรอบและเริ่มมี residue
|
||||
- Recommendation: cleanup imports เมื่อมีรอบแก้ implementation
|
||||
|
||||
---
|
||||
|
||||
## Missing Features
|
||||
|
||||
- `location type`
|
||||
- `online URL`
|
||||
- duplicate warning
|
||||
- single-file certificate enforcement
|
||||
- conditional certificate-required validation/messaging
|
||||
- create-specific error/partial-success UX สำหรับกรณี create สำเร็จแต่ upload ล้มเหลว
|
||||
|
||||
---
|
||||
|
||||
## Improvement Opportunities
|
||||
|
||||
- แยก canonical employee-create flow ออกจาก edit/review semantics ให้ชัดกว่า component เดียว
|
||||
- ทำ business rules เป็น explicit contract ใน `types.ts` และ `schema` ก่อนเพิ่ม field ใหม่
|
||||
- เพิ่ม clearer post-submit state สำหรับ attachment upload
|
||||
- หาก requirement online/offline ต่างกันจริง ให้ใช้ conditional form sections แทน flat form ปัจจุบัน
|
||||
|
||||
---
|
||||
|
||||
## Final Assessment
|
||||
|
||||
Architecture: 8.0/10
|
||||
Functionality: 6.3/10
|
||||
Role & Permission: 8.2/10
|
||||
UI: 7.3/10
|
||||
UX: 6.6/10
|
||||
Accessibility: 7.0/10
|
||||
Security: 8.1/10
|
||||
Performance: 7.4/10
|
||||
Maintainability: 7.1/10
|
||||
Overall Score: 7.3/10
|
||||
|
||||
335
docs/review/employee-dashboard-audit.md
Normal file
335
docs/review/employee-dashboard-audit.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Employee Dashboard Audit Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Overall Status
|
||||
|
||||
- Good
|
||||
|
||||
ภาพรวม:
|
||||
|
||||
- หน้า Employee Dashboard ใช้สถาปัตยกรรมที่สอดคล้องกับโปรเจ็กต์ค่อนข้างดี โดยมี route guard, server-side data aggregation และแยก component ตาม feature ชัดเจน
|
||||
- ข้อมูลหลักของพนักงาน เช่น ชั่วโมงที่อนุมัติแล้ว ความคืบหน้า K / S / A กิจกรรมล่าสุด และประกาศล่าสุด ถูกดึงจากฝั่ง server ภายใต้ขอบเขตองค์กรและผู้ใช้ที่กำลังล็อกอิน
|
||||
- จุดที่ควรปรับหลัก ๆ อยู่ที่ error handling, ความสม่ำเสมอของ navigation/UX, การแสดงผล progress เมื่อเกินเป้า, และการสื่อสารกรณีข้อมูลว่างหรือผู้ใช้ยังไม่ผูก employee profile
|
||||
|
||||
---
|
||||
|
||||
## Architecture Review
|
||||
|
||||
สิ่งที่พบ:
|
||||
|
||||
- Route หลักอยู่ที่ [src/app/dashboard/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/page.tsx) และ redirect ไป `/dashboard/overview`
|
||||
- Employee Dashboard จริงถูกวางบน [src/app/dashboard/overview/layout.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/layout.tsx) และ [src/app/dashboard/overview/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/page.tsx)
|
||||
- Layout ใช้ parallel routes แยก slot เป็น `@area_stats`, `@sales`, `@bar_stats`, `@pie_stats`
|
||||
- Data หลักรวมอยู่ใน [src/features/overview/server/overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts) และใช้ `cache()` ลดการคำนวณซ้ำใน render เดียวกัน
|
||||
- Guard ใช้ [src/lib/auth/page-guards.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/lib/auth/page-guards.ts) ผ่าน `requireEmployeeDashboardAccess()`
|
||||
|
||||
ข้อสรุป:
|
||||
|
||||
- โครงสร้างโดยรวมดี และแยก server responsibility ไว้ถูกที่
|
||||
- อย่างไรก็ดี Employee Dashboard ถูกวางชื่อ route เป็น `overview` มากกว่าจะมี bounded context ชัดเจนแบบ `employee-dashboard` ทำให้ความหมายของหน้าและความต่างระหว่าง employee กับ HRD พึ่งพา conditional rendering ค่อนข้างมาก
|
||||
|
||||
---
|
||||
|
||||
## Functional Review
|
||||
|
||||
| Feature | Status | Notes |
|
||||
| ------- | ------ | ----- |
|
||||
| View own dashboard | Pass | พนักงานเข้า dashboard ได้ผ่าน `requireEmployeeDashboardAccess()` |
|
||||
| Summary cards | Pass | มี approved, pending/revision, rejected, progress |
|
||||
| Required hours resolution | Partial | fallback ไป policy ล่าสุดหรือ 0 ได้ แต่ไม่มีข้อความอธิบายเมื่อไม่พบ policy/target |
|
||||
| K / S / A summary | Pass | คิดจาก approved records เท่านั้น ซึ่งสอดคล้องกับการวัดความคืบหน้า |
|
||||
| Recent activities | Partial | แสดง 5 รายการล่าสุด แต่เรียงตาม `createdAt` ไม่ใช่ `trainingDate` |
|
||||
| Announcements widget | Pass | แสดงประกาศล่าสุดและลิงก์ไปหน้ารายละเอียดได้ |
|
||||
| Notifications page | Pass | มีหน้ารายการ notifications แยกต่างหาก |
|
||||
| Progress bar | Partial | รองรับ 0 และ partial ได้ แต่กรณีเกินเป้าถูก cap ที่ 100% ทำให้ไม่เห็นว่าทะลุเป้า |
|
||||
| Empty states | Partial | มี empty state ในหลาย widget แต่ไม่มีคำอธิบายกรณีผู้ใช้ยังไม่ผูก employee profile |
|
||||
| Error states | Partial | มี error boundary ครบหลายจุด แต่แสดง raw `error.message` ต่อผู้ใช้ |
|
||||
| Responsive layout | Pass | layout ใช้ grid ที่ยุบลงได้ทั้ง mobile และ desktop |
|
||||
| Dashboard filter | Partial | employee กรองได้ตามปี, admin กรองได้หลายมิติ แต่ใช้ native select และ submit form แบบพื้นฐาน |
|
||||
| Refresh behavior | Partial | ข้อมูลดึงจาก server render ใหม่ได้ แต่หน้า dashboard ไม่มี evidence ชัดเจนเรื่อง targeted invalidation หลัง action อื่น ๆ |
|
||||
|
||||
---
|
||||
|
||||
## Business Rule Review
|
||||
|
||||
สิ่งที่สอดคล้อง:
|
||||
|
||||
- Employee เห็นเฉพาะข้อมูลตนเองผ่าน `getEmployeeIdsForUser()` และเงื่อนไข scope ใน [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts)
|
||||
- ชั่วโมง approved ใช้ `approvedHours` ถ้ามี ไม่เช่นนั้น fallback ไป `hours`
|
||||
- K / S / A คิดจากรายการที่อนุมัติแล้วเท่านั้น
|
||||
- ปีที่ใช้กรองแยกจากค่า default ปีปัจจุบัน
|
||||
|
||||
จุดที่ควรระวัง:
|
||||
|
||||
- การใช้ปีอ้างอิงด้วย `new Date().getUTCFullYear()` และช่วงวันแบบ UTC อาจต่างจากการตีความปีตาม timezone ธุรกิจที่ใช้งานจริงปลายปี/ต้นปี
|
||||
- หากพนักงานไม่มี employee profile ที่ผูกกับ user ระบบจะคืนค่าศูนย์เกือบทั้งหมด แต่ไม่ได้บอกผู้ใช้ว่าสาเหตุคือ “ยังไม่พบข้อมูลพนักงาน”
|
||||
- ความคืบหน้าถูกจำกัดไม่เกิน 100% ทำให้ business outcome แบบ “เกินเป้า” ไม่ถูกสื่อสารบน UI
|
||||
|
||||
---
|
||||
|
||||
## Role & Permission Review
|
||||
|
||||
Employee
|
||||
|
||||
- View own dashboard: Pass
|
||||
- View own statistics: Pass
|
||||
- View own training history: Pass
|
||||
- View notifications: Pass ผ่าน [src/app/dashboard/notifications/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/notifications/page.tsx)
|
||||
- View announcements: Pass ผ่าน [src/app/dashboard/announcements/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/page.tsx)
|
||||
- View other employees: Not observed in dashboard flow
|
||||
- Approve training / edit policy / access HRD pages: Guarded by HRD-only routes
|
||||
|
||||
ข้อสังเกต:
|
||||
|
||||
- ฝั่ง route protection ถือว่าดี
|
||||
- แต่ navigation ไม่สม่ำเสมอ: employee เข้า announcements และ notifications ได้โดยตรง แต่เมนูใน [src/config/nav-config.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts) ไม่ได้เปิดเส้นทางเหล่านี้ให้เห็นอย่างชัดเจนใน sidebar หลัก
|
||||
|
||||
---
|
||||
|
||||
## UI Review
|
||||
|
||||
จุดที่ดี:
|
||||
|
||||
- ใช้ `PageContainer`, `Card`, `Badge`, `Progress` และ chart shell เดียวกับโปรเจ็กต์
|
||||
- spacing โดยรวมสม่ำเสมอ
|
||||
- widget หลักแบ่งเป็น summary + content blocks อ่านง่ายบน desktop
|
||||
|
||||
จุดที่ควรปรับ:
|
||||
|
||||
- summary cards render `Badge` ทุกใบ แม้หลายใบส่ง `badge: ''` มา ทำให้มี badge ว่างหรือเหลือเพียง icon ใน [src/app/dashboard/overview/layout.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/layout.tsx)
|
||||
- filter panel ใช้ native `<select>` แทน shared form/select patterns ของโปรเจ็กต์
|
||||
- หน้า employee dashboard ไม่มี notification widget โดยตรง ทั้งที่มี notifications page อยู่แล้ว ทำให้ข้อมูลสำคัญถูกแยกบริบทออกจากหน้าหลัก
|
||||
|
||||
---
|
||||
|
||||
## UX Review
|
||||
|
||||
จุดที่ดี:
|
||||
|
||||
- ผู้ใช้เห็นภาพรวมข้อมูลสำคัญได้เร็ว
|
||||
- ประกาศล่าสุดและกิจกรรมล่าสุดช่วยสร้างบริบทการใช้งานประจำวัน
|
||||
|
||||
จุดที่ควรปรับ:
|
||||
|
||||
- หากเกิด error ผู้ใช้จะเห็นข้อความเชิงเทคนิคเกินไป
|
||||
- หากไม่มีข้อมูลเพราะยังไม่ผูก employee profile, ไม่มี policy, หรือยังไม่เคยส่งรายการ ระบบไม่อธิบายสาเหตุให้ชัด
|
||||
- notifications แยกไปอีกหน้า แต่ไม่มีทางเข้าที่เด่นพอจาก dashboard/sidebar สำหรับ employee
|
||||
- recent activities ใช้วันฝึกอบรมแสดงผล แต่ใช้เวลาสร้างรายการเป็นตัวเรียงลำดับ ซึ่งอาจขัดกับความคาดหวังว่า “ล่าสุด” หมายถึงฝึกเมื่อไร
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Review
|
||||
|
||||
จุดที่ดี:
|
||||
|
||||
- ใช้ semantic button/link/card ของ shared UI หลายจุด
|
||||
- chart components เปิด `accessibilityLayer` ใน Recharts หลายตัว
|
||||
|
||||
จุดที่ควรปรับ:
|
||||
|
||||
- error UI หลายจุดพึ่งพาสีและข้อความทั่วไป แต่ไม่มี recovery action ครบทุก slot
|
||||
- native chart summary ไม่มี text alternative เชิงสรุปมากนักสำหรับ screen reader
|
||||
- badge/progress สื่อสถานะหลักด้วย visual emphasis มากกว่าคำอธิบายเฉพาะกรณี “เกินเป้า”
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Review
|
||||
|
||||
จุดที่ดี:
|
||||
|
||||
- รวม logic ฝั่ง dashboard ไว้ใน data builder เดียว ทำให้ trace business logic ได้ง่าย
|
||||
- type ค่อนข้างชัด และ model ที่ส่งไป component แยกตามการใช้งานจริง
|
||||
- ใช้ helper role กลาง เช่น `isOrganizationAdminRole()`
|
||||
|
||||
จุดที่ควรปรับ:
|
||||
|
||||
- `OverviewFilterPanel` เช็ก admin role ด้วย string comparison ตรง ๆ แทน helper กลาง
|
||||
- มีการผสม concern ของ employee/admin dashboard ไว้ใน route เดียวค่อนข้างมาก
|
||||
- naming บางส่วนยังติด template เดิม เช่น `RecentSales` ทั้งที่ใช้แสดงกิจกรรมการอบรม
|
||||
|
||||
---
|
||||
|
||||
## API Review
|
||||
|
||||
สิ่งที่พบ:
|
||||
|
||||
- Dashboard data ฝั่ง overview ดึงตรงจาก server helper ไม่ผ่าน client query เป็นหลัก
|
||||
- Notifications ใช้ route handlers และ React Query ตาม pattern โปรเจ็กต์
|
||||
- Notifications API validate query params ด้วย Zod
|
||||
|
||||
จุดที่ควรปรับ:
|
||||
|
||||
- error responses จาก API ถูกส่งกลับไปยัง UI แล้ว render `error.message` ตรง ๆ ในบางหน้าของ dashboard/notifications
|
||||
- dashboard เองยังไม่มี contract ที่แยก concern เช่น announcements กับ notifications ให้ชัดในหน้าหลัก
|
||||
|
||||
---
|
||||
|
||||
## Performance Review
|
||||
|
||||
จุดที่ดี:
|
||||
|
||||
- `getOverviewDashboardData()` ถูกห่อด้วย `cache()` ช่วยลดงานซ้ำใน request เดียว
|
||||
- widget หลักเป็น server-driven ลด client state ที่ไม่จำเป็น
|
||||
|
||||
จุดที่ควรระวัง:
|
||||
|
||||
- หน้า overview มีหลาย parallel route ที่ต่างเรียก data helper เดียวกัน แม้ `cache()` จะช่วย แต่โครงสร้างยังค่อนข้างซับซ้อนสำหรับ dashboard ที่ข้อมูลสัมพันธ์กันสูง
|
||||
- chart และ card ทั้งหมดโหลดพร้อมกัน ไม่มี loading boundary ระดับหน้า root ของ overview
|
||||
|
||||
---
|
||||
|
||||
## Security Review
|
||||
|
||||
จุดที่ดี:
|
||||
|
||||
- route protection ฝั่ง employee/HRD ใช้งานจริง
|
||||
- dashboard scope ข้อมูลตาม organization และ user
|
||||
- notifications API จำกัดตาม `organizationId` และ `userId`
|
||||
|
||||
ความเสี่ยง:
|
||||
|
||||
- error boundary หลายจุดเปิดเผย `error.message` ตรง ๆ เช่น [src/app/dashboard/overview/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/error.tsx), [src/app/dashboard/overview/@sales/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@sales/error.tsx), [src/features/notifications/components/notifications-page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/components/notifications-page.tsx)
|
||||
|
||||
---
|
||||
|
||||
## Consistency Review
|
||||
|
||||
สอดคล้อง:
|
||||
|
||||
- ใช้ shared UI stack และ page guard ตามแนวทางโปรเจ็กต์
|
||||
- notifications page ใช้ React Query + HydrationBoundary ตาม pattern กลาง
|
||||
|
||||
ไม่สอดคล้อง:
|
||||
|
||||
- dashboard table-like recent activity ใช้ custom list ซึ่งโอเคตามลักษณะข้อมูล แต่ naming ยังโยงกับ template เดิม
|
||||
- filter panel ของ overview ยังไม่ใช้ shared select/form wrappers แบบที่หลาย feature ใช้อยู่
|
||||
- employee-accessible pages บางหน้าไม่มีเมนูนำทางที่สอดคล้องกับสิทธิ์จริง
|
||||
|
||||
---
|
||||
|
||||
## Edge Case Review
|
||||
|
||||
- New employee: แสดงค่า 0 ได้ แต่ไม่บอกว่าเพราะยังไม่มีข้อมูลพนักงาน/ประวัติอบรม
|
||||
- No training records: รองรับได้
|
||||
- All completed: รองรับได้
|
||||
- Over target: คำนวณได้ แต่ UI ไม่สื่อส่วนเกิน
|
||||
- Missing category: K / S / A จะเหลือค่าตามข้อมูลที่มี แต่ไม่ได้อธิบาย category ที่หาย
|
||||
- Missing policy: fallback เป็น 0 ได้ แต่ไม่มี warning/explanation
|
||||
- Missing profile: ไม่พัง แต่ silent failure ในเชิง UX
|
||||
- API failure: มี error UI แต่ข้อความไม่เหมาะกับผู้ใช้ปลายทาง
|
||||
- Slow network: มี loading เฉพาะบาง slot แต่ไม่มี root loading ของ overview
|
||||
|
||||
---
|
||||
|
||||
## Problems Found
|
||||
|
||||
### High
|
||||
|
||||
- Description: Error UI หลายจุดแสดง raw `error.message` ต่อผู้ใช้โดยตรง
|
||||
- File: [overview/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/error.tsx), [@sales/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@sales/error.tsx), [@area_stats/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@area_stats/error.tsx), [@pie_stats/error.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/@pie_stats/error.tsx), [notifications-page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/notifications/components/notifications-page.tsx)
|
||||
- Component: `OverviewError`, `SalesError`, `AreaStatsError`, `PieStatsError`, `NotificationsPage`
|
||||
- Severity: High
|
||||
- Impact: เสี่ยงเปิดเผยข้อมูลเชิงเทคนิคจาก backend/runtime และทำให้ UX ของผู้ใช้ปลายทางสับสน
|
||||
- Recommendation: แสดงข้อความกลางที่ปลอดภัยต่อผู้ใช้ และ log รายละเอียดไว้ภายในแทน
|
||||
|
||||
- Description: Dashboard ไม่สื่อกรณี “เกินเป้าชั่วโมง” แม้ข้อมูลคำนวณถึงเป้าแล้ว
|
||||
- File: [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts), [ksa-progress.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/ksa-progress.tsx)
|
||||
- Component: `buildEmployeeCards`, `KsaProgress`
|
||||
- Severity: High
|
||||
- Impact: ผู้ใช้ที่ทำเกินเป้าจะเห็นเพียง 100% เท่ากับผู้ใช้ที่เพิ่งแตะเป้า ทำให้สูญเสียข้อมูลเชิงบริหารและแรงจูงใจ
|
||||
- Recommendation: แยกสถานะ completed กับ exceeded และสื่อส่วนเกินบน UI
|
||||
|
||||
### Medium
|
||||
|
||||
- Description: Recent activities เรียงด้วย `createdAt` แต่แสดงวันที่เป็น `trainingDate`
|
||||
- File: [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts), [recent-sales.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/recent-sales.tsx)
|
||||
- Component: `getOverviewDashboardData`, `RecentSales`
|
||||
- Severity: Medium
|
||||
- Impact: ผู้ใช้อาจเข้าใจว่าเป็น “อบรมล่าสุดตามวันที่ฝึก” ทั้งที่จริงคือ “รายการที่สร้างล่าสุด”
|
||||
- Recommendation: เลือกให้ชัดว่าจะเรียงตามวันฝึกหรือวันส่ง และทำ label ให้ตรง
|
||||
|
||||
- Description: พนักงานเข้าหน้า notifications และ announcements ได้ แต่ navigation หลักไม่สอดคล้องกับสิทธิ์จริง
|
||||
- File: [nav-config.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/config/nav-config.ts), [notifications/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/notifications/page.tsx), [announcements/page.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/announcements/page.tsx)
|
||||
- Component: sidebar config, employee-accessible routes
|
||||
- Severity: Medium
|
||||
- Impact: discoverability ต่ำ ผู้ใช้เข้าฟีเจอร์ที่มีสิทธิ์ได้ยาก และ navigation model ไม่สม่ำเสมอ
|
||||
- Recommendation: ทบทวนเมนู employee ให้ตรงกับ route ที่เปิดใช้งานจริง
|
||||
|
||||
- Description: Summary cards แสดง badge แม้ไม่มีข้อความ badge จริง
|
||||
- File: [overview/layout.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview/layout.tsx)
|
||||
- Component: `OverViewLayout`
|
||||
- Severity: Medium
|
||||
- Impact: UI ดูไม่ตั้งใจและเพิ่ม noise ในส่วนสรุปสำคัญ
|
||||
- Recommendation: render badge แบบมีเงื่อนไข หรือกำหนดข้อความ badge ให้ครบ
|
||||
|
||||
- Description: กรณี user ไม่มี employee profile / ไม่มี policy ระบบแสดงศูนย์อย่างเงียบ ๆ
|
||||
- File: [overview-data.ts](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/server/overview-data.ts)
|
||||
- Component: `getOverviewDashboardData`
|
||||
- Severity: Medium
|
||||
- Impact: ผู้ใช้และทีม support แยกไม่ออกว่า “ไม่มีข้อมูล” หรือ “ข้อมูลเชื่อมไม่ครบ”
|
||||
- Recommendation: เพิ่ม state หรือ messaging ที่อธิบายสาเหตุของข้อมูลว่าง
|
||||
|
||||
### Low
|
||||
|
||||
- Description: `OverviewFilterPanel` ใช้ string compare role ตรง ๆ แทน helper กลาง
|
||||
- File: [overview-filter-panel.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/overview-filter-panel.tsx)
|
||||
- Component: `OverviewFilterPanel`
|
||||
- Severity: Low
|
||||
- Impact: ลดความสม่ำเสมอของโค้ดและเพิ่มโอกาส logic drift
|
||||
- Recommendation: ใช้ role helper กลางเดียวกับส่วนอื่น
|
||||
|
||||
- Description: ชื่อ component `RecentSales` ไม่ตรงกับหน้าที่จริง
|
||||
- File: [recent-sales.tsx](/d:/WY-2569/HRD/training-system-minimal-refactor/src/features/overview/components/recent-sales.tsx)
|
||||
- Component: `RecentSales`
|
||||
- Severity: Low
|
||||
- Impact: อ่านโค้ดยากขึ้นและสะท้อนร่องรอยจาก template เดิม
|
||||
- Recommendation: เปลี่ยนชื่อให้ตรงกับโดเมนเมื่อมีรอบ refactor
|
||||
|
||||
- Description: หน้า overview ไม่มี root loading state ของ route หลัก
|
||||
- File: [src/app/dashboard/overview](/d:/WY-2569/HRD/training-system-minimal-refactor/src/app/dashboard/overview)
|
||||
- Component: overview route
|
||||
- Severity: Low
|
||||
- Impact: initial load ของทั้งหน้าอาจดูนิ่งเกินไปหาก data ช้า และพึ่ง loading ของบาง slot เท่านั้น
|
||||
- Recommendation: เพิ่ม loading ระดับหน้าให้สอดคล้องกับหน้า dashboard สำคัญอื่น ๆ
|
||||
|
||||
---
|
||||
|
||||
## Missing Features
|
||||
|
||||
- Notification summary/widget บนหน้า dashboard สำหรับ employee
|
||||
- ข้อความอธิบายกรณีไม่พบ employee profile หรือ policy เป้าหมาย
|
||||
- การสื่อสารสถานะ “เกินเป้า” แยกจาก “ครบเป้า”
|
||||
- Navigation entry ที่สอดคล้องกับ employee-accessible notifications/announcements
|
||||
|
||||
---
|
||||
|
||||
## Improvement Opportunities
|
||||
|
||||
- แยก employee dashboard semantics ให้ชัดกว่าการ reuse `overview` route ร่วมกับ HRD
|
||||
- เพิ่ม domain-specific naming ให้ component/slot เพื่อให้อ่านโค้ดง่ายขึ้น
|
||||
- ทำ filter panel ให้ใช้ shared form/select stack ของโปรเจ็กต์
|
||||
- เพิ่ม explanatory empty states ตามสาเหตุจริงของข้อมูลว่าง
|
||||
- เพิ่ม text summary สำหรับ charts เพื่อช่วยทั้ง accessibility และการอ่านค่าบน mobile
|
||||
|
||||
---
|
||||
|
||||
## Final Assessment
|
||||
|
||||
Architecture: 8/10
|
||||
|
||||
Functionality: 7.5/10
|
||||
|
||||
UI: 7/10
|
||||
|
||||
UX: 6.8/10
|
||||
|
||||
Accessibility: 6.8/10
|
||||
|
||||
Performance: 7.5/10
|
||||
|
||||
Security: 7.5/10
|
||||
|
||||
Maintainability: 7.6/10
|
||||
|
||||
Overall Score: 7.3/10
|
||||
401
docs/review/employee-online-lessons-audit.md
Normal file
401
docs/review/employee-online-lessons-audit.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# Employee Online Lessons Audit Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Overall Status
|
||||
|
||||
- Needs Improvement
|
||||
|
||||
The employee-facing online lessons module follows the project's core architecture well: server-guarded dashboard routes, feature-local API contracts, React Query prefetch/hydration, and organization-scoped route handlers are all in place. Employee access to unpublished lessons is also enforced server-side, not only in the UI.
|
||||
|
||||
The main gaps are functional completeness and employee experience. The current implementation delivers a published lesson list and detail page, but the scope in `plans/audit-employee-online-lessons.md` expects more than content viewing. There is no employee progress tracking, no completion state, no learning history, no dashboard integration for lesson completion, no certificate section, no dedicated loading/error routes, and only basic attachment/video fallback handling. The module is structurally solid, but the employee learning workflow is still only partially implemented.
|
||||
|
||||
---
|
||||
|
||||
## Scope Reviewed
|
||||
|
||||
Routes
|
||||
|
||||
- `src/app/dashboard/online-lessons/page.tsx`
|
||||
- `src/app/dashboard/online-lessons/[id]/page.tsx`
|
||||
- `src/app/dashboard/online-lessons/manage/new/page.tsx`
|
||||
- `src/app/dashboard/online-lessons/manage/[id]/page.tsx`
|
||||
- `src/app/api/online-lessons/route.ts`
|
||||
- `src/app/api/online-lessons/[id]/route.ts`
|
||||
|
||||
Feature components and contracts
|
||||
|
||||
- `src/features/online-lessons/components/online-lessons-listing.tsx`
|
||||
- `src/features/online-lessons/components/online-lessons-page.tsx`
|
||||
- `src/features/online-lessons/components/online-lesson-detail-page.tsx`
|
||||
- `src/features/online-lessons/components/online-lesson-form.tsx`
|
||||
- `src/features/online-lessons/components/online-lesson-action-menu.tsx`
|
||||
- `src/features/online-lessons/api/types.ts`
|
||||
- `src/features/online-lessons/api/service.ts`
|
||||
- `src/features/online-lessons/api/queries.ts`
|
||||
- `src/features/online-lessons/api/mutations.ts`
|
||||
- `src/features/online-lessons/server/online-lesson-data.ts`
|
||||
- `src/features/online-lessons/schemas/online-lesson.ts`
|
||||
|
||||
Shared/auth/navigation/storage
|
||||
|
||||
- `src/lib/auth/page-guards.ts`
|
||||
- `src/lib/auth/session.ts`
|
||||
- `src/lib/auth/roles.ts`
|
||||
- `src/lib/online-lesson-storage.ts`
|
||||
- `src/config/nav-config.ts`
|
||||
- `docs/AI_DEVELOPMENT_GUIDE.md`
|
||||
- `docs/PROJECT_ARCHITECTURE.md`
|
||||
|
||||
---
|
||||
|
||||
## Functional Review
|
||||
|
||||
| Feature | Status | Notes |
|
||||
| ------- | ------ | ----- |
|
||||
| Lesson list loads | Pass | `page.tsx` and `online-lessons-listing.tsx` use server prefetch + `HydrationBoundary`, then `useSuspenseQuery()` in `online-lessons-page.tsx`. |
|
||||
| Pagination | Pass | Backed by `page` and `limit` params and returned from `/api/online-lessons`. |
|
||||
| Search | Pass | Employee list supports text search through query state and server filtering. |
|
||||
| Category filter | Pass | Employee list supports category filtering; categories are scoped server-side. |
|
||||
| Sorting | Partial | Server sorts by published status and publish/create dates, but there is no employee-facing sort control. |
|
||||
| Responsive cards | Fail | The employee list is a `DataTable`, not lesson cards. |
|
||||
| Empty state | Pass | The page renders a clear empty state with filter reset support. |
|
||||
| Loading state | Partial | There is inline fetch feedback in the list, but no route-level `loading.tsx` for list or detail. |
|
||||
| Error state | Fail | No route-level `error.tsx` exists under `src/app/dashboard/online-lessons`. |
|
||||
| Lesson detail basics | Partial | Title, description, category, published date, thumbnail, and attachment links exist. Instructor and duration do not. |
|
||||
| Uploaded video support | Pass | Route validation and detail rendering support uploaded MP4, WebM, and MOV. |
|
||||
| YouTube support | Pass | Detail page converts standard, short, shorts, and embed URLs into iframe embeds. |
|
||||
| Video fallback UI | Partial | Non-embeddable external URLs fall back to an open-link card, but uploaded-video failure handling is minimal. |
|
||||
| Attachment handling | Partial | Open/download links exist, but there is no inline image or PDF preview. |
|
||||
| Certificate section | Fail | No certificate field or UI exists in the employee lesson detail. |
|
||||
| Progress display | Fail | No lesson progress model, API field, or UI is present. |
|
||||
| Completed status | Fail | No completion badge or completion tracking exists. |
|
||||
| Learning history | Fail | No viewed/completed/recent lesson history is implemented. |
|
||||
| Dashboard integration | Fail | No evidence that online lesson completion feeds employee dashboard metrics or training hours. |
|
||||
|
||||
---
|
||||
|
||||
## Business Rule Review
|
||||
|
||||
What is aligned
|
||||
|
||||
- Employee lesson pages use `requireEmployeeDashboardAccess()` or the organization-scoped API layer before rendering or returning data.
|
||||
- Employee lesson queries are filtered to published lessons only through `listOnlineLessonsForAccess()` using `eq(onlineLessons.isPublished, true)` for non-HRD users.
|
||||
- Direct detail access is also server-scoped through `getOnlineLessonByIdForAccess()`, so unpublished lessons are not exposed by URL guessing alone.
|
||||
- Create, update, delete, and publish/archive actions are protected by `requireHRD()` in route handlers and `requireHRDDashboardAccess()` in manage pages.
|
||||
|
||||
Inconsistencies or gaps
|
||||
|
||||
- The business rule protection for employee visibility depends on `isPublished` being kept in sync with `status`. The implementation does keep them aligned today, but the access rule is coupled to that derived flag instead of checking `status === 'published'` directly.
|
||||
- The audit scope expects progress, completion, history, dashboard updates, training-hour rules, and certificate visibility rules, but there is no runtime contract for those concepts anywhere in the online lessons feature.
|
||||
|
||||
---
|
||||
|
||||
## Role & Permission Review
|
||||
|
||||
Employee permissions
|
||||
|
||||
- View lesson list: Pass
|
||||
- View lesson detail: Pass
|
||||
- Watch embedded or uploaded video: Pass
|
||||
- Search lessons: Pass
|
||||
- Filter by category: Pass
|
||||
- Download allowed files: Pass
|
||||
- Create/edit/delete/publish lessons: Guarded server-side and route-side
|
||||
- Access HRD manage pages: Guarded
|
||||
- Access hidden or draft lessons directly: Guarded server-side
|
||||
|
||||
HRD/Admin permissions
|
||||
|
||||
- Manage pages are limited to HRD through `requireHRDDashboardAccess()`.
|
||||
- Mutating API endpoints are limited to HRD through `requireHRD()`.
|
||||
|
||||
Notes
|
||||
|
||||
- UI permission separation is generally clean. Employee list rows only show a single "open lesson" action, while HRD users additionally see status/creator columns and the action menu.
|
||||
- The main employee route also exposes the "add online lesson" CTA when the signed-in user is HRD. This is reasonable for a shared route, but it means the page mixes employee and management affordances in one surface.
|
||||
|
||||
---
|
||||
|
||||
## UI Review
|
||||
|
||||
Strengths
|
||||
|
||||
- The page uses `PageContainer`, card sections, and shared dashboard primitives consistent with the rest of the app.
|
||||
- Thumbnail, metadata, badges, and buttons are visually consistent with the existing design system.
|
||||
- Empty state handling is clearer than many legacy areas because search/filter reset remains available.
|
||||
|
||||
Weaknesses
|
||||
|
||||
- The employee list is table-centric, while the plan explicitly calls for responsive lesson cards. On mobile, this is usable but less content-first than a card layout.
|
||||
- The detail page lacks dedicated sections for instructor, duration, progress, completion, certificate availability, or related learning context.
|
||||
- Attachment UX is basic. Users can open/download files, but there is no preview affordance for images or PDFs.
|
||||
- There is no loading skeleton or route-level loading view for the employee route family.
|
||||
|
||||
---
|
||||
|
||||
## UX Review
|
||||
|
||||
Strengths
|
||||
|
||||
- Discovery is straightforward for published lessons: search, category filter, and a single row CTA keep the main flow simple.
|
||||
- Detail-page video behavior covers the common YouTube URL shapes and also supports uploaded files.
|
||||
|
||||
Weaknesses
|
||||
|
||||
- The module currently behaves more like a content library than a learning workflow.
|
||||
- Users do not get progress feedback, watched/completed state, or "continue where you left off" guidance.
|
||||
- Missing video, broken uploaded video, and broken embed cases do not provide especially strong recovery guidance.
|
||||
- The detail page exposes creator/status metadata but omits learner-oriented metadata such as duration and completion cues.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Review
|
||||
|
||||
What is good
|
||||
|
||||
- Buttons and links are based on shared UI primitives, so baseline keyboard and focus behavior should be reasonable.
|
||||
- Thumbnail images include `alt` text derived from lesson title.
|
||||
- Native video controls are available for uploaded videos.
|
||||
|
||||
Risks
|
||||
|
||||
- The `<track kind="captions" />` element in the uploaded-video player has no `src`, so captions are not actually implemented.
|
||||
- There is no explicit accessibility treatment for attachment previews because previews do not exist yet.
|
||||
- Video error states and fallback messaging are limited, which can leave assistive users with less context when media fails.
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Review
|
||||
|
||||
Strengths
|
||||
|
||||
- The feature follows the expected app-owned structure with `api/types.ts`, `service.ts`, `queries.ts`, `mutations.ts`, schema validation, and server data helpers.
|
||||
- Route handlers keep persistence and authorization logic on the server.
|
||||
- Query invalidation is simple and predictable through `onlineLessonKeys`.
|
||||
|
||||
Weaknesses
|
||||
|
||||
- `online-lessons-page.tsx` is doing list rendering, filters, columns, empty state, and role-based behavior in one client component, which makes the employee list surface larger than necessary.
|
||||
- The employee-facing feature has no shared learner-progress contracts, so expanding the scope later will likely require contract changes across types, routes, and UI together.
|
||||
- There is no shared video/attachment display component even though the plan suggests richer media behavior that may need reuse.
|
||||
|
||||
---
|
||||
|
||||
## API Review
|
||||
|
||||
Strengths
|
||||
|
||||
- `/api/online-lessons` and `/api/online-lessons/[id]` enforce organization access and HRD-only mutation rules.
|
||||
- List endpoint supports pagination, search, category filtering, and status filtering for managers.
|
||||
- File validation for video, attachment, and thumbnail uploads is handled server-side before storage.
|
||||
|
||||
Weaknesses
|
||||
|
||||
- Employee-facing API contracts do not include progress, completion, last-viewed, certificate, duration, or instructor data.
|
||||
- There is no separate endpoint for attachment download authorization; attachments are exposed by stored file URL once the lesson payload is returned.
|
||||
- Video and attachment runtime errors are left mostly to browser-native behavior instead of richer API/UI recovery states.
|
||||
|
||||
---
|
||||
|
||||
## Security Review
|
||||
|
||||
Strengths
|
||||
|
||||
- Route handlers use shared auth helpers instead of trusting client state.
|
||||
- Employee direct access to hidden/draft lessons is blocked in both list and detail server queries.
|
||||
- HRD-only mutations are enforced in API handlers and manage pages.
|
||||
- Storage helper code validates file types, size limits, and upload-path safety.
|
||||
|
||||
Risks
|
||||
|
||||
- Lesson assets appear to be served from public upload URLs. That is convenient, but it means asset access is not re-authorized per request once a URL is known.
|
||||
- The access policy is derived from `isPublished`; if future code ever desynchronizes `status` and `isPublished`, employee visibility could drift from the intended business rule.
|
||||
|
||||
---
|
||||
|
||||
## Performance Review
|
||||
|
||||
Strengths
|
||||
|
||||
- List and detail pages use server prefetch + React Query hydration.
|
||||
- Pagination is server-driven, which keeps the list payload bounded.
|
||||
- Detail pages fetch only one lesson at a time.
|
||||
|
||||
Weaknesses
|
||||
|
||||
- The detail page uses raw `<img>` instead of `next/image`, so thumbnail optimization is limited.
|
||||
- Uploaded video elements do not appear to use lazy or staged loading strategies beyond browser defaults.
|
||||
- There is no dedicated progressive loading UX for media-heavy content on slow networks.
|
||||
|
||||
---
|
||||
|
||||
## Data Validation Review
|
||||
|
||||
What is covered
|
||||
|
||||
- Title is required through `onlineLessonSchema`.
|
||||
- Status is restricted by schema.
|
||||
- Video URL must be an `http` or `https` URL when provided.
|
||||
- Route handlers enforce that at least one video source exists on create and retained update flows.
|
||||
- File type and file size checks exist for uploaded video, attachment, and thumbnail files.
|
||||
|
||||
What is missing
|
||||
|
||||
- No validation exists for instructor, duration, certificate rules, or learner progress because those fields are absent from the feature model.
|
||||
- No YouTube-specific validation exists beyond generic URL parsing and best-effort embed conversion.
|
||||
|
||||
---
|
||||
|
||||
## Edge Case Review
|
||||
|
||||
- No lessons: handled with an empty state.
|
||||
- One lesson: should work cleanly with the current list/detail flow.
|
||||
- Many lessons: pagination exists, but the table-first UI is less optimized for content browsing than cards.
|
||||
- Missing thumbnail: handled with a placeholder block on the detail page.
|
||||
- Missing video: handled with a "no video" message.
|
||||
- Broken YouTube link: partial handling only; non-embeddable links get a fallback card, but broken embeds still depend on iframe/browser behavior.
|
||||
- Broken uploaded video: partial handling only; there is no custom error state or recovery UI.
|
||||
- Missing attachment: handled by omitting the attachment section.
|
||||
- Invalid PDF/image preview: not applicable because inline previews are not implemented.
|
||||
- Slow network: partial handling only; fetch feedback exists, but there are no dedicated loading routes or skeletons.
|
||||
- API failure: no dedicated route error boundary exists for the employee online-lessons routes.
|
||||
- Unauthorized direct access to hidden lessons: guarded server-side.
|
||||
- Archived lesson via direct URL: guarded as long as `isPublished` remains false for archived lessons.
|
||||
|
||||
---
|
||||
|
||||
## Consistency Review
|
||||
|
||||
Consistent with project patterns
|
||||
|
||||
- Uses `PageContainer`, local feature contracts, route handlers, Drizzle-backed server helpers, React Query prefetch, and shared auth/page guard helpers.
|
||||
- Reuses shared dashboard UI primitives and badges.
|
||||
- Follows the feature-module architecture documented in `docs/AI_DEVELOPMENT_GUIDE.md` and `docs/PROJECT_ARCHITECTURE.md`.
|
||||
|
||||
Not fully consistent
|
||||
|
||||
- The list UI uses `DataTable`, but it does not reuse the richer shared `DataTableToolbar` pattern described in the project guide.
|
||||
- The employee online lesson experience is much lighter than nearby employee-facing areas such as dashboard and training records, especially around loading/error states and learner feedback.
|
||||
|
||||
---
|
||||
|
||||
## Problems Found
|
||||
|
||||
### Critical
|
||||
|
||||
- None found.
|
||||
|
||||
### High
|
||||
|
||||
- Description: The employee online lessons feature has no implementation for progress tracking, completion state, learning history, certificate availability, or dashboard integration, even though these are core audit-scope expectations.
|
||||
- File: `src/features/online-lessons/api/types.ts`, `src/features/online-lessons/components/online-lessons-page.tsx`, `src/features/online-lessons/components/online-lesson-detail-page.tsx`, `src/app/api/online-lessons/route.ts`, `src/app/api/online-lessons/[id]/route.ts`
|
||||
- Component: API contracts, employee list page, employee detail page, list/detail route handlers
|
||||
- Severity: High
|
||||
- Impact: The module supports browsing lessons, but not the employee learning lifecycle expected by the plan. Downstream reporting and dashboard experiences cannot reflect lesson completion because the feature model does not capture it.
|
||||
- Evidence: `serializeOnlineLesson()` only returns content/admin metadata in `src/features/online-lessons/server/online-lesson-data.ts:31`; there are no progress/completion fields in `src/features/online-lessons/api/types.ts`; employee list/detail components render no such state.
|
||||
- Recommendation: Define learner-state contracts first, then add server-side tracking, employee UI surfaces, and dashboard/report integration on top of that contract.
|
||||
|
||||
### Medium
|
||||
|
||||
- Description: There are no dedicated `loading.tsx` or `error.tsx` route files for the employee online-lessons routes.
|
||||
- File: `src/app/dashboard/online-lessons`
|
||||
- Component: employee list and detail route tree
|
||||
- Severity: Medium
|
||||
- Impact: Slow-network and failure states are less consistent with the rest of the dashboard and provide weaker recovery UX.
|
||||
- Evidence: No `loading.tsx` or `error.tsx` files exist under `src/app/dashboard/online-lessons`; list page only shows inline fetching text and detail relies on default route behavior.
|
||||
- Recommendation: Add route-level loading and error surfaces consistent with the app's employee-facing patterns.
|
||||
|
||||
- Description: The employee lesson detail page does not include instructor, duration, certificate, or progress-oriented sections, while it does expose management-oriented metadata such as creator name and publication status.
|
||||
- File: `src/features/online-lessons/components/online-lesson-detail-page.tsx`
|
||||
- Component: `OnlineLessonDetailPage`
|
||||
- Severity: Medium
|
||||
- Impact: The page gives weaker learning context than expected and prioritizes admin metadata over learner metadata.
|
||||
- Evidence: The metadata block renders `status`, `category`, `published_at`, and `created_by_name` around `src/features/online-lessons/components/online-lesson-detail-page.tsx:111`, but no instructor/duration/progress/certificate fields exist.
|
||||
- Recommendation: Rebalance the detail layout around employee learning needs and move non-essential admin metadata out of the primary reading flow.
|
||||
|
||||
- Description: Attachment handling is limited to link-based open/download actions and does not implement inline image preview, PDF preview, or unsupported-file guidance.
|
||||
- File: `src/features/online-lessons/components/online-lesson-detail-page.tsx`
|
||||
- Component: attachment section
|
||||
- Severity: Medium
|
||||
- Impact: Learners have less confidence about what a file contains before opening it, and the module does not meet the richer attachment scope in the audit plan.
|
||||
- Evidence: The attachment block only renders file name plus open/download buttons around `src/features/online-lessons/components/online-lesson-detail-page.tsx:208`.
|
||||
- Recommendation: Add file-type-aware previews and clearer unsupported-file messaging where preview is not possible.
|
||||
|
||||
- Description: Uploaded-video failure handling is minimal, and the captions track is not actually configured.
|
||||
- File: `src/features/online-lessons/components/online-lesson-detail-page.tsx`
|
||||
- Component: uploaded video player
|
||||
- Severity: Medium
|
||||
- Impact: Broken media leads to a weak recovery path, and caption accessibility is not functional.
|
||||
- Evidence: The uploaded-video block uses native `<video controls>` with a blank `<track kind="captions" />` around `src/features/online-lessons/components/online-lesson-detail-page.tsx:158`.
|
||||
- Recommendation: Add explicit media error UI and only render captions tracks when real caption files exist.
|
||||
|
||||
- Description: Lesson assets are served from public upload URLs rather than protected download endpoints.
|
||||
- File: `src/lib/online-lesson-storage.ts`, `src/features/online-lessons/server/online-lesson-data.ts`
|
||||
- Component: lesson asset storage and serialized payload
|
||||
- Severity: Medium
|
||||
- Impact: Once a client receives a file URL, asset access is no longer re-checked per request.
|
||||
- Evidence: Serialized lesson payload includes `video_file_url`, `attachment_url`, and `thumbnail_url` from storage-backed public paths in `src/features/online-lessons/server/online-lesson-data.ts:38-43`.
|
||||
- Recommendation: If the business wants stricter asset protection, move file delivery behind authorized download endpoints or signed URLs.
|
||||
|
||||
### Low
|
||||
|
||||
- Description: The employee list experience is table-based rather than card-based, which is less aligned with content browsing and the audit plan's responsive-card expectation.
|
||||
- File: `src/features/online-lessons/components/online-lessons-page.tsx`
|
||||
- Component: `OnlineLessonsPage`
|
||||
- Severity: Low
|
||||
- Impact: Usability is acceptable, but the presentation is less content-first on mobile and less aligned with lesson-library expectations.
|
||||
- Evidence: The list renders a `DataTable` with columns and row CTAs rather than lesson cards.
|
||||
- Recommendation: Consider a dedicated employee card layout if lesson discovery and media browsing become a priority.
|
||||
|
||||
---
|
||||
|
||||
## Missing Features
|
||||
|
||||
- Lesson progress display
|
||||
- Completed lesson state
|
||||
- Remaining lessons or progress calculation
|
||||
- Completion badge
|
||||
- Learning history for viewed/completed/recent lessons
|
||||
- Dashboard integration for lesson completion or training-hour impact
|
||||
- Certificate availability section
|
||||
- Instructor metadata
|
||||
- Duration metadata
|
||||
- Attachment previews for images and PDFs
|
||||
- Dedicated route-level loading UI
|
||||
- Dedicated route-level error UI
|
||||
- Stronger broken-video fallback and recovery UX
|
||||
|
||||
---
|
||||
|
||||
## Improvement Opportunities
|
||||
|
||||
- Separate the employee learning experience from the HRD management experience more clearly, especially in detail-page metadata and list presentation.
|
||||
- Introduce learner-state contracts at the API layer before adding UI for progress/history/completion.
|
||||
- Reuse a richer shared toolbar/filter pattern if the employee list gains more discovery controls.
|
||||
- Consider protected delivery for lesson assets if access revocation or stricter confidentiality becomes important.
|
||||
- Extract video and attachment presentation into reusable components if the feature expands to richer media states.
|
||||
|
||||
---
|
||||
|
||||
## Final Assessment
|
||||
|
||||
Architecture: 8.2/10
|
||||
|
||||
Functionality: 6.7/10
|
||||
|
||||
Role & Permission: 8.4/10
|
||||
|
||||
UI: 7.0/10
|
||||
|
||||
UX: 6.6/10
|
||||
|
||||
Accessibility: 6.8/10
|
||||
|
||||
Performance: 7.4/10
|
||||
|
||||
Security: 7.7/10
|
||||
|
||||
Maintainability: 7.8/10
|
||||
|
||||
Overall Score: 7.3/10
|
||||
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
|
||||
297
docs/review/pending-review-audit.md
Normal file
297
docs/review/pending-review-audit.md
Normal 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
|
||||
Reference in New Issue
Block a user