25 KiB
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.tsxsrc/app/dashboard/training-records/[trainingRecordId]/page.tsxsrc/lib/auth/page-guards.ts
- Form/components:
src/features/training-records/components/training-record-form.tsxsrc/features/training-records/components/training-record-view-page.tsxsrc/features/training-records/components/training-record-course-combobox.tsx
- Validation/types/query/mutation:
src/features/training-records/schemas/training-record.tssrc/features/training-records/api/types.tssrc/features/training-records/api/service.tssrc/features/training-records/api/queries.tssrc/features/training-records/api/mutations.tssrc/features/training-records/api/certificate-mutations.ts
- API/server/security:
src/app/api/training-records/route.tssrc/app/api/training-records/[id]/certificates/route.tssrc/features/training-records/server/training-record-data.tssrc/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:00hours: ถูก block00:15hours: รองรับ01:45hours: รองรับ30:00hours: รองรับ- 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 typeonline 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