# 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