# 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

