# Fix User Training Filters — ประเภทการอบรมและสถานะไม่กรองข้อมูลจริง ## เป้าหมาย แก้ไขปัญหาในหน้าผู้ใช้งานที่แสดงรายการประวัติการอบรม โดยตัวกรองต่อไปนี้ไม่สามารถกรองข้อมูลได้จริง - ประเภทการอบรม - สถานะรายการ หลังแก้ไขแล้ว เมื่อผู้ใช้งานเลือกค่า Filter ระบบต้องแสดงเฉพาะข้อมูลที่ตรงกับเงื่อนไขที่เลือก และต้องทำงานร่วมกับ Search, Pagination, Sorting และ Filter อื่นที่มีอยู่เดิมได้อย่างถูกต้อง --- ## ปัญหาปัจจุบัน ในหน้ารายการประวัติการอบรมของผู้ใช้งาน มี UI สำหรับเลือก Filter ดังนี้ 1. ประเภทการอบรม 2. สถานะ แต่เมื่อเลือกค่าแล้วพบว่า: - ตารางยังแสดงข้อมูลทั้งหมด - ข้อมูลในตารางไม่เปลี่ยนแปลง - ไม่มีการ Refresh หรือ Refetch ข้อมูล - Filter อาจถูกเปลี่ยนเฉพาะใน UI แต่ไม่ได้ส่งไปยัง API - Backend อาจรับค่า Filter แต่ไม่ได้นำไปใช้ใน Query - ค่าใน Select อาจไม่ตรงกับค่าที่เก็บในฐานข้อมูล - Query Key หรือ URL Search Params อาจไม่มีค่า Filter อยู่ด้วย --- ## ขอบเขตการตรวจสอบ ตรวจสอบ Flow ทั้งหมดตั้งแต่ UI จนถึง Database โดยห้ามแก้เฉพาะหน้า UI หาก Backend ยังไม่รองรับการกรองจริง Flow ที่ต้องตรวจสอบ: ```text Filter UI → Filter State → URL Search Params → API Request → API Validation → Service / Repository → Database Query → API Response → Table Rendering ``` --- ## 1. ตรวจสอบหน้ารายการของผู้ใช้งาน ค้นหาหน้ารายการประวัติการอบรมของผู้ใช้งาน เช่น: ```text /dashboard/training-records /dashboard/my-training-records /dashboard/user/training-records ``` หรือ Route ที่ระบบใช้งานจริง ตรวจสอบ Component ที่เกี่ยวข้อง เช่น: ```text TrainingRecordList TrainingRecordTable TrainingRecordToolbar TrainingRecordFilters UserTrainingRecordTable ``` ต้องตรวจสอบว่าตัวกรองทั้งสองเชื่อมกับ State จริง ไม่ใช่แสดง UI อย่างเดียว --- ## 2. Filter ประเภทการอบรม ตรวจสอบว่า Filter ประเภทการอบรมใช้ Field ใดในระบบจริง เช่น: ```ts trainingType; training_type; type; trainingMode; training_mode; ``` ค่าที่เลือกจาก UI ต้องตรงกับค่าที่เก็บในฐานข้อมูล ตัวอย่างค่าที่เป็นไปได้: ```ts online; offline; ``` หรือ: ```ts internal; external; ``` ห้ามสมมติค่าขึ้นใหม่ ให้ตรวจสอบจาก Schema, Enum, Zod Schema, TypeScript Type และข้อมูลใน Database ก่อน ตัวอย่างโครงสร้าง Select: ```tsx ``` ค่า `"all"` ต้องใช้สำหรับล้างเงื่อนไขเท่านั้น และไม่ควรถูกส่งไปกรองใน Database ตัวอย่างการสร้าง Filter: ```ts const filters = { page, limit, ...(search && { search }), ...(trainingType && trainingType !== "all" && { trainingType, }), }; ``` --- ## 3. Filter สถานะ ตรวจสอบสถานะที่ระบบรองรับจริง เช่น: ```ts draft; pending; approved; rejected; returned; cancelled; ``` ห้ามกำหนดรายการสถานะจากการคาดเดา ให้ตรวจสอบจาก: - Database Schema - Enum - Zod Schema - TypeScript Type - Status Badge - Workflow ปัจจุบันของระบบ - API Endpoint ที่เกี่ยวข้อง ตัวอย่าง Select: ```tsx ``` ตัวอย่างการสร้าง Filter: ```ts const filters = { page, limit, ...(search && { search }), ...(status && status !== "all" && { status }), }; ``` --- ## 4. เชื่อม Filter กับ URL Search Params หากหน้าปัจจุบันใช้ URL Search Params ให้ใช้รูปแบบเดียวกับ Filter อื่นที่มีอยู่เดิม ตัวอย่าง URL: ```text /dashboard/training-records?trainingType=online&status=pending&page=1 ``` เมื่อเปลี่ยน Filter ต้อง: 1. อัปเดต Search Params 2. Reset หน้า Pagination กลับเป็นหน้า 1 3. Refetch หรือ Reload ข้อมูลตามค่าปัจจุบัน 4. ไม่ลบ Search Params อื่นที่ยังใช้งานอยู่ ตัวอย่าง: ```ts const handleTrainingTypeChange = (value: string) => { const params = new URLSearchParams(searchParams.toString()); if (value === "all") { params.delete("trainingType"); } else { params.set("trainingType", value); } params.set("page", "1"); router.replace(`${pathname}?${params.toString()}`); }; ``` ทำแนวทางเดียวกันกับ `status` --- ## 5. ตรวจสอบ Query Key และการ Refetch หากใช้ React Query หรือ TanStack Query ต้องนำค่า Filter เข้าไปอยู่ใน `queryKey` ตัวอย่างที่ไม่ถูกต้อง: ```ts queryKey: ["training-records"]; ``` ตัวอย่างที่ควรเป็น: ```ts queryKey: [ "training-records", { page, limit, search, trainingType, status, sort, }, ]; ``` หากใช้ SWR ต้องนำ Query String หรือ Filter เข้าไปอยู่ใน Key เช่นกัน ```ts const queryString = new URLSearchParams({ page: String(page), limit: String(limit), ...(search && { search }), ...(trainingType && trainingType !== "all" && { trainingType, }), ...(status && status !== "all" && { status, }), }).toString(); const { data } = useSWR(`/api/training-records?${queryString}`, fetcher); ``` เมื่อ Filter เปลี่ยน ระบบต้อง Refetch อัตโนมัติ โดยไม่ต้องกด Refresh หน้า Browser --- ## 6. ตรวจสอบ API Request ตรวจสอบว่า Request ส่งค่า Filter ไปยัง API จริง ตัวอย่าง: ```text GET /api/training-records?trainingType=online&status=pending&page=1&limit=10 ``` ตรวจสอบผ่าน: - Browser Network Tab - Server Log - API Handler - Query Parser ห้ามส่งค่า: ```text trainingType=all status=all ``` หากเลือก “ทั้งหมด” ต้องไม่ส่ง Parameter นั้น หรือส่งเป็น `undefined` --- ## 7. ตรวจสอบ API Validation เพิ่มหรือแก้ไข Schema สำหรับ Query Parameters ให้รองรับ Filter ทั้งสอง ตัวอย่าง: ```ts const trainingRecordQuerySchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(10), search: z.string().trim().optional(), trainingType: trainingTypeSchema.optional(), status: trainingRecordStatusSchema.optional(), sort: z.string().optional(), }); ``` ควรใช้ Enum หรือ Schema ที่มีอยู่เดิม ห้ามสร้างค่าซ้ำหลายจุด ตัวอย่าง: ```ts const trainingTypeSchema = z.enum(["online", "offline"]); const trainingRecordStatusSchema = z.enum([ "draft", "pending", "approved", "rejected", ]); ``` รายการจริงต้องอ้างอิงจากระบบปัจจุบัน หากได้รับค่าที่ไม่ถูกต้อง ควรตอบกลับด้วย Validation Error ที่เหมาะสม ไม่ควร silently ignore Filter --- ## 8. ตรวจสอบ Service หรือ Repository นำค่าที่ผ่าน Validation แล้วส่งต่อไปยัง Service หรือ Repository ตัวอย่าง: ```ts const result = await getTrainingRecords({ userId: currentUser.id, page: query.page, limit: query.limit, search: query.search, trainingType: query.trainingType, status: query.status, sort: query.sort, }); ``` ต้องรักษาเงื่อนไขสำคัญของหน้าผู้ใช้งาน คือผู้ใช้งานต้องเห็นเฉพาะข้อมูลของตนเอง ตัวอย่าง: ```ts where: { employeeId: currentEmployee.id, ...(trainingType && { trainingType, }), ...(status && { status, }), } ``` ห้ามแก้ Filter แล้วทำให้ผู้ใช้งานสามารถเห็นข้อมูลของพนักงานคนอื่นได้ --- ## 9. ตรวจสอบ Database Query หากใช้ Prisma ให้เพิ่มเงื่อนไข Filter ใน `where` ตัวอย่าง: ```ts const where: Prisma.TrainingRecordWhereInput = { employeeId: currentEmployee.id, ...(search && { OR: [ { courseName: { contains: search, mode: "insensitive", }, }, { providerName: { contains: search, mode: "insensitive", }, }, ], }), ...(trainingType && { trainingType, }), ...(status && { status, }), }; ``` จากนั้นใช้ `where` ชุดเดียวกันทั้ง Query ข้อมูลและ Query นับจำนวน ```ts const [items, total] = await Promise.all([ prisma.trainingRecord.findMany({ where, skip, take: limit, orderBy, }), prisma.trainingRecord.count({ where, }), ]); ``` ต้องใช้เงื่อนไขเดียวกันทั้ง `findMany` และ `count` เพื่อไม่ให้ Pagination แสดงจำนวนผิด หากใช้ SQL โดยตรง ต้องเพิ่ม Parameterized Query และห้ามต่อ SQL String แบบไม่ปลอดภัย --- ## 10. รองรับการใช้หลาย Filter พร้อมกัน Filter ต้องทำงานแบบ AND ตัวอย่าง: ```text ประเภทการอบรม = ออนไลน์ สถานะ = รอตรวจสอบ ``` ผลลัพธ์ต้องแสดงเฉพาะรายการที่: ```text trainingType = online AND status = pending ``` และยังต้องทำงานร่วมกับ Search เช่น: ```text search = React AND trainingType = online AND status = pending ``` --- ## 11. Reset Pagination เมื่อ Filter เปลี่ยน เมื่อผู้ใช้งานอยู่หน้า 3 แล้วเปลี่ยน Filter ระบบต้องกลับไปหน้า 1 เพื่อป้องกัน Empty State ที่เกิดจาก Page เดิมไม่มีข้อมูล ตัวอย่าง: ```ts onValueChange={(value) => { setTrainingType(value); setPagination((previous) => ({ ...previous, pageIndex: 0, })); }} ``` หากใช้ URL: ```ts params.set("page", "1"); ``` --- ## 12. ปุ่มล้าง Filter หากหน้าปัจจุบันมีปุ่ม Reset หรือ Clear Filter ต้องล้างค่าทั้งสองได้จริง ค่าที่ต้องล้าง: - Search - ประเภทการอบรม - สถานะ - Filter อื่นที่อยู่ใน Toolbar ตามพฤติกรรมเดิม - Pagination กลับไปหน้า 1 ตัวอย่าง: ```ts const clearFilters = () => { setSearch(""); setTrainingType("all"); setStatus("all"); setPage(1); }; ``` หากใช้ Search Params ต้องลบ Parameter ที่เกี่ยวข้องออกจาก URL ```ts params.delete("trainingType"); params.delete("status"); params.set("page", "1"); ``` --- ## 13. Loading และ Empty State เมื่อ Filter เปลี่ยน: - แสดง Loading State ตามมาตรฐานเดิมของระบบ - ป้องกันการกด Filter ซ้ำระหว่าง Pending หากจำเป็น - ไม่แสดงข้อมูลเก่าที่ทำให้ผู้ใช้เข้าใจผิด - หลังโหลดเสร็จต้องแสดงข้อมูลใหม่ทันที เมื่อไม่มีข้อมูลตรงกับ Filter ให้แสดงข้อความ เช่น: ```text ไม่พบรายการประวัติการอบรมที่ตรงกับเงื่อนไข ``` ควรมีปุ่ม: ```text ล้างตัวกรอง ``` ไม่ควรใช้ข้อความทั่วไปว่า “ไม่มีข้อมูล” หากมี Filter ทำงานอยู่ เพราะผู้ใช้อาจเข้าใจว่าไม่มีประวัติการอบรมทั้งหมด --- ## 14. ตรวจสอบ Mapping ของ Label และ Value ต้องแยกค่าที่แสดงกับค่าที่ส่งไป Backend อย่างชัดเจน ตัวอย่าง: ```ts const TRAINING_TYPE_OPTIONS = [ { label: "ทั้งหมด", value: "all", }, { label: "ออนไลน์", value: "online", }, { label: "ออฟไลน์", value: "offline", }, ]; ``` ห้ามใช้ Label ภาษาไทยเป็น Database Value เว้นแต่ Schema ปัจจุบันเก็บค่าเป็นภาษาไทยจริง ตัวอย่างที่ควรหลีกเลี่ยง: ```ts ออนไลน์ ``` หาก Database เก็บค่าเป็น: ```text online ``` --- ## 15. ใช้ Constant และ Type กลางของระบบ หากมี Constant หรือ Enum อยู่แล้ว ให้ใช้ของเดิม เช่น: ```ts TRAINING_TYPES; TRAINING_RECORD_STATUSES; TrainingType; TrainingRecordStatus; ``` ห้ามประกาศรายการซ้ำแยกกันใน: - Filter UI - API Schema - Status Badge - Form - Database Query เพราะอาจทำให้ค่าไม่ตรงกันในอนาคต ควรมี Source of Truth เดียวสำหรับ: - Value - Label - Description - Badge Variant - Permission หรือ Workflow ที่เกี่ยวข้อง --- ## 16. ห้ามเปลี่ยน Business Rule เดิม การแก้ไขครั้งนี้เป็นการแก้ Filter เท่านั้น ห้ามเปลี่ยนโดยไม่จำเป็น: - Workflow การอนุมัติ - Permission - สิทธิ์ของ Employee - สิทธิ์ของ HRD - การสร้างหรือแก้ไขรายการ - Status Transition - Sorting เริ่มต้น - รูปแบบ Pagination - Response Structure ของ API - UI Style หลักของระบบ หน้าผู้ใช้งานต้องยังคงเห็นเฉพาะรายการของตนเองตาม Permission เดิม --- ## 17. Test Cases ที่ต้องตรวจสอบ ### Test Case 1: Filter ประเภทการอบรม 1. เปิดหน้ารายการประวัติการอบรม 2. เลือกประเภท “ออนไลน์” 3. ตรวจสอบว่าทุกรายการเป็นประเภทออนไลน์ 4. เปลี่ยนเป็น “ออฟไลน์” 5. ตรวจสอบว่าทุกรายการเป็นประเภทออฟไลน์ 6. เลือก “ทุกประเภท” 7. ตรวจสอบว่าแสดงข้อมูลทุกประเภท ### Test Case 2: Filter สถานะ 1. เลือกสถานะ “รอตรวจสอบ” 2. ตรวจสอบว่าทุกรายการมีสถานะรอตรวจสอบ 3. เปลี่ยนเป็น “อนุมัติแล้ว” 4. ตรวจสอบว่าทุกรายการมีสถานะอนุมัติแล้ว 5. เลือก “ทุกสถานะ” 6. ตรวจสอบว่าแสดงข้อมูลทุกสถานะที่ผู้ใช้มีสิทธิ์เห็น ### Test Case 3: ใช้สอง Filter พร้อมกัน 1. เลือกประเภทออนไลน์ 2. เลือกสถานะรอตรวจสอบ 3. ตรวจสอบว่าทุกรายการตรงทั้งสองเงื่อนไข ### Test Case 4: Filter ร่วมกับ Search 1. กรอกคำค้นหา 2. เลือกประเภทการอบรม 3. เลือกสถานะ 4. ตรวจสอบว่าข้อมูลตรงทุกเงื่อนไข ### Test Case 5: Filter ร่วมกับ Pagination 1. เลือก Filter ที่มีหลายหน้า 2. เปลี่ยนหน้า Pagination 3. ตรวจสอบว่า Filter ยังคงอยู่ 4. เปลี่ยน Filter 5. ตรวจสอบว่าระบบกลับมาหน้า 1 ### Test Case 6: Refresh หน้า หากระบบใช้ URL Search Params: 1. เลือกประเภทและสถานะ 2. Refresh Browser 3. ตรวจสอบว่าค่า Filter ยังคงอยู่ 4. ข้อมูลต้องยังตรงกับเงื่อนไขเดิม ### Test Case 7: Clear Filter 1. เลือกหลาย Filter 2. กดล้างตัวกรอง 3. ตรวจสอบว่าค่าทั้งหมดถูก Reset 4. ตารางกลับมาแสดงข้อมูลทั้งหมด 5. Pagination กลับมาหน้า 1 ### Test Case 8: ไม่มีข้อมูลตรงกับ Filter 1. เลือกเงื่อนไขที่ไม่มีข้อมูล 2. ต้องแสดง Empty State ที่เหมาะสม 3. ปุ่มล้างตัวกรองต้องใช้งานได้ ### Test Case 9: Permission 1. Login ด้วย Employee 2. ใช้ Filter ทุกประเภท 3. ตรวจสอบว่าไม่เห็นรายการของพนักงานคนอื่น ### Test Case 10: Count และ Pagination 1. เลือก Filter 2. ตรวจสอบจำนวนรายการทั้งหมด 3. ตรวจสอบจำนวนหน้า 4. จำนวนต้องสัมพันธ์กับข้อมูลหลังกรอง ไม่ใช่จำนวนทั้งหมดก่อนกรอง --- ## Acceptance Criteria งานถือว่าเสร็จสมบูรณ์เมื่อ: - Filter ประเภทการอบรมกรองข้อมูลได้จริง - Filter สถานะกรองข้อมูลได้จริง - Filter ทั้งสองใช้งานพร้อมกันได้ - ทำงานร่วมกับ Search ได้ - ทำงานร่วมกับ Sorting ได้ - ทำงานร่วมกับ Pagination ได้ - เปลี่ยน Filter แล้วข้อมูล Refresh หรือ Refetch อัตโนมัติ - เปลี่ยน Filter แล้ว Pagination กลับหน้า 1 - เลือก “ทั้งหมด” แล้วล้างเงื่อนไขได้จริง - ปุ่มล้างตัวกรองทำงานถูกต้อง - จำนวนรายการและจำนวนหน้าหลังกรองถูกต้อง - Frontend และ Backend ใช้ค่า Enum ตรงกัน - ไม่มีการส่งค่า `"all"` ไปใช้ใน Database Query - ผู้ใช้งานยังเห็นเฉพาะข้อมูลของตนเอง - ไม่มี Regression ต่อ Filter หรือ Workflow อื่น - TypeScript, Lint และ Build ผ่าน - มีการเพิ่มหรือปรับปรุง Test ตามรูปแบบที่ Repository ใช้อยู่ --- ## สิ่งที่ต้องส่งมอบ หลังดำเนินการเสร็จ ให้สรุปผลดังนี้: 1. สาเหตุที่ทำให้ Filter ไม่ทำงาน 2. ไฟล์ที่แก้ไข 3. Filter ใช้ Field และ Value ใด 4. การเปลี่ยนแปลงฝั่ง Frontend 5. การเปลี่ยนแปลงฝั่ง API และ Backend 6. วิธี Reset Filter และ Pagination 7. Test Cases ที่ดำเนินการ 8. ผลการรัน Type Check, Lint, Test และ Build 9. ประเด็นที่ยังไม่ได้แก้หรือข้อจำกัด หากมี ห้ามรายงานว่าแก้ไขสำเร็จโดยตรวจสอบเฉพาะ UI ต้องยืนยันจาก API Request และผลลัพธ์ข้อมูลจริงด้วย