Files
alla-tms/docs/review/employee-create-training-record-audit.md
2026-07-16 09:53:14 +07:00

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.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