# แผน N9 — กลุ่มเอกสารตาม `typeId` จาก e-Court + หน้ารายการเอกสารในกลุ่ม > สถานะ: **implement แล้ว (2026-09-23)** — `tsc` + vitest 142/142 + Playwright ผ่านทุกข้อ verify ของ task 3–4 + plan-reviewer/code-reviewer ไม่พบ violation (คงเหลือ e2e ลิงก์ e-Court จริงตามแผน ตอนปิดงาน N8) — เขียนใหม่ 2026-09-22 แทนร่างเดิม "Document Set จัดชุดมือใน UI" (2026-09-17, commit 6940b38) ตามคำขอผู้ใช้: ให้ e-Court บอกกลุ่มผ่านลิงก์แทนการจัดชุดด้วยมือ > เพิ่มเติม (2026-09-23 ตามคำขอผู้ใช้): การ์ดไฟล์หน้า `/groups/[typeId]` หุบ/กางดู bookmark+annotation ทั้งไฟล์ (ปิด gap 5.3.10 ครึ่งหลังที่เคยเป็น out of scope) + ย่อข้อความหมายเหตุเหลือ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" > > Redesign (2026-09-24 ตามคำขอผู้ใช้): หน้า `/groups/[typeId]` เปลี่ยนจากการ์ดต่อไฟล์เป็น **explorer 2 คอลัมน์ ไม่มี navbar** — แผงตัวกรองฝั่งซ้าย (ไฟล์ / ชนิดรายการ bookmark|annotation / ชนิดของ Annotation / ผู้บันทึก) + list เดียวรวม bookmark+annotation ทั้งกลุ่มไม่แยกไฟล์ (รายละเอียด D3) — ผ่าน client components `components/groups/` 3 ตัว > เพิ่มเติม (2026-09-23 ตามคำขอผู้ใช้): ฟอร์มอัปโหลด N4 เพิ่มช่อง **"กลุ่ม typeId"** ทางเลือก (ไม่กรอก = ไม่มีกลุ่ม) — `uploadFile` กลายเป็นจุดเขียน `type_id` ที่สองนอกจาก N8 (insert ตอนสร้าง row ใหม่เท่านั้น normalize เหมือน N8: trim → ว่าง = NULL, cap 200) — ยังไม่มี UI แก้กลุ่มของไฟล์ที่มีอยู่เดิม > เลขฟีเจอร์: ร่างเดิม (2026-09-17) เคยใช้เลข N8 — ยกให้แผนลิงก์ e-Court (`docs/PLAN-ECOURT.md`) ซึ่งเป็น dependency ต้นทาง แผนนี้จึงเป็น **N9** > Dependency: **N8** — การรับ/refresh `typeId` ของเอกสารจากลิงก์เกิดที่ `registerExternalFile` ของ N8 ทั้งหมด (เดิมแผนนี้ไม่มี code path ที่เขียน `type_id` เอง — ต่อมาเพิ่มช่องกลุ่มของฟอร์มอัปโหลด N4 ตามคำขอผู้ใช้ 2026-09-23) — **N8 implement ครบแล้ว (2026-09-22 รวม D5)**: schema `type_id` + index มากับ migration `drizzle/0002` ด้วย > ตาม governance ของโปรเจกต์: implement ต้องเขียนสเปกลง `docs/PLAN-FEAT.md` §7 (**N9**) + ขยาย whitelist ใน `CLAUDE.md` + อัปเดต `docs/PLAN.md` §3 (schema) ก่อน — เป็น task แรกของแผน แล้วรันผ่าน `/implement-feat N9` ## Context - กรณีจริงของศาล: คดีหนึ่งงวด = หลายเอกสาร (คำฟ้อง / คำพิพากษา / คำสั่งศาล) — ผู้ใช้อยากเห็นเอกสารที่เกี่ยวเนื่องกันจัดกลุ่มมาแสดง และมีหน้ารวมทุกเอกสารของกลุ่ม - ร่างเดิม (2026-09-17) ออกแบบเป็นตาราง `document_sets` + ย้ายไฟล์เข้า/ออกด้วยมือใน UI — ผู้ใช้ตัดสินใจใหม่ (2026-09-22): ให้ e-Court ส่งรหัสกลุ่มมากับลิงก์ (`?typeId=` — เปลี่ยนชื่อจาก caseId ตามคำขอ) จึง**ไม่ต้องมีตารางแยก / Server Actions / UI จัดการกลุ่มเลย** เหลือแค่ column + การแสดงผล - ข้อจำกัดที่ยอมรับ (ผู้ใช้ยืนยัน 2026-09-22): ระบบเรารู้จักเฉพาะเอกสารที่เคยเปิดผ่านลิงก์ e-Court (row เกิดตอน `registerExternalFile`) — หน้ากลุ่มจึงแสดง**เฉพาะเอกสารที่รู้จักแล้ว** (เคยมีหมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้" กำกับ — ถอดออกตามคำขอผู้ใช้ 2026-09-24; รายการครบทั้งคดีอยู่ใน e-Court ไม่ใช่ระบบเรา) ## การตัดสินใจ (ยืนยันกับผู้ใช้แล้ว 2026-09-22) 1. **กลุ่มระดับเดียว กำหนดด้วย `typeId` จากลิงก์ e-Court** — เพิ่ม column บนตาราง `files` ค่าเดียว (`type_id` — ถอด `type_name` ตามคำขอผู้ใช้ 2026-09-22, แสดงผลด้วยรหัสกลุ่มโดยตรง) — ไม่มีตารางกลุ่มแยก ไม่มี many-to-many ไม่มีหลายระดับ (เอกสารหนึ่งอยู่กลุ่มเดียวตามธรรมชาติของงาน) — ความหมายของ id เป็นของ e-Court ฝั่งเราถือว่า "ส่งมาเหมือนกัน = กลุ่มเดียวกัน" 2. **เข้ามาอัตโนมัติ ไม่มี UI จัดการกลุ่ม** — ไฟล์ได้กลุ่มเมื่อเปิดผ่านลิงก์ที่มี `typeId` (เขียนที่ N8) หรือกรอกช่อง "กลุ่ม typeId" ตอนอัปโหลด N4 (เพิ่ม 2026-09-23) — กลุ่มเกิด/หายตามข้อมูลจริงใน `files` ไม่มีสถานะกลุ่มค้างในตารางอื่น ไม่มีหน้าทางย้ายไฟล์ที่มีอยู่เดิมเข้า/ออกกลุ่ม 3. **หน้ารายการ `/` จัดกลุ่มตาม `typeId`** (section ต่อกลุ่ม + ส่วน "ไม่มีกลุ่ม" ท้ายสุด) + **หน้าใหม่ `/groups/[typeId]`** แสดงเอกสารทั้งกลุ่มที่รู้จัก พร้อมหมายเหตุกำกับ (ถอดออก 2026-09-24 ตามคำขอผู้ใช้) และการ์ดแต่ละไฟล์หุบ/กางดู bookmark+annotation ของไฟล์นั้นได้ (2026-09-23 — รูปแบบเปลี่ยนเป็น explorer ใน redesign 2026-09-24) ## Design ### D1 — Schema (`db/schema.ts` + `drizzle/`) ```ts // files เพิ่ม (เอกสารจากลิงก์ e-Court / อัปโหลด N4 ที่กรอกกลุ่ม — ไฟล์ seed และอัปโหลดที่ไม่กรอก = NULL เสมอ): typeId: text("type_id"), // + index ("files_type_id_idx") บน type_id ``` - ไม่มี FK/ตาราง `document_sets` (ต่างจากร่างเดิม) — กลุ่ม = ค่า distinct ของ `type_id` ณ ตอน query - migration: `/sync-db` (push) + **`npx drizzle-kit generate`** — rule data-layer: Docker migrate จาก SQL ใน `drizzle/` เท่านั้น; ไฟล์ generated idempotent (`IF NOT EXISTS`) - การเขียน `type_id` สองที่: N8 `registerExternalFile` (insert ครั้งแรก + update เมื่อลิงก์รอบใหม่ส่ง `typeId` มา — ดู PLAN-ECOURT.md D1) และ `uploadFile` ของ N4 ช่องกลุ่มทางเลือก (insert ตอนสร้าง row ใหม่เท่านั้น — เพิ่ม 2026-09-23) — แผนนี้ (N9) **อ่านอย่างเดียว** - ไม่มี DTO/mapper ใหม่ — หน้าที่ใช้เป็น server component ทั้งหมด ข้อมูลข้ามไป client เป็น plain object `{id, title, typeId}` (ถ้าต้องส่ง `createdAt` ข้าม boundary = ISO string ตาม invariant #5) ### D2 — หน้ารายการ `/` จัดกลุ่ม (`app/page.tsx`) - query `files` เหมือนเดิม → แบ่งเป็น section ต่อกลุ่ม (`type_id` ไม่ NULL) เรียงตาม `createdAt` ของไฟล์ล่าสุดในกลุ่ม (กลุ่มที่มี activity ใหม่อยู่บน) — การ์ดไฟล์ในกลุ่มเรียง `createdAt` เหมือนเดิม + ปุ่ม Annotate เดิม - หัวกลุ่ม = `type_id` + จำนวนไฟล์ + **ลิงก์ "เอกสารในกลุ่ม" → `/groups/{encodeURIComponent(typeId)}`** (icon Folder) - ไฟล์ที่ `type_id` NULL (seed / อัปโหลด N4 ที่ไม่กรอกกลุ่ม / ลิงก์แบบไม่มี typeId) → section **"ไม่มีกลุ่ม"** ไว้ท้ายสุด (ใกล้ฟอร์มเพิ่มเอกสาร) — แสดงเสมอถ้ามีไฟล์ - ไม่มี client component ใหม่ ไม่มี action ใหม่ — จัดกลุ่มเป็นการแสดงผลล้วนบน server component เดิม ### D3 — หน้า `/groups/[typeId]` แสดงเอกสารในกลุ่ม (`app/groups/[typeId]/page.tsx`) Server component `force-dynamic` (login ถูกบังคับโดย `middleware.ts` อยู่แล้ว — path ใหม่เข้า net อัตโนมัติ): - โหลด: `files where type_id = params.typeId` `orderBy createdAt` (Next decode dynamic param ให้อัตโนมัติ — **อย่าเรียก `decodeURIComponent` ซ้ำ** ตาม edge case #5) — **ไม่มี = 404** (กลุ่มที่ระบบไม่รู้จัก/ย้ายไฟล์ออกหมดแล้ว) - แสดง: หัวกลุ่ม (`type_id` + "N เอกสาร") + ลิงก์กลับ `/` - ~~**หมายเหตุกำกับทุกครั้ง**: "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้"~~ — **ถอดออกตามคำขอผู้ใช้ 2026-09-24** (เดิมเป็นกล่อง info ใต้หัวกลุ่ม ย่อจากข้อความ "… — อาจไม่ครบทุกฉบับของกลุ่ม" 2026-09-23) - รายการเอกสาร: ชื่อไฟล์ + วันที่นำเข้า (`formatThaiDate`) + จำนวน annotation/bookmark + ปุ่ม/ลิงก์ **Annotate → `/annotate/{fileId}`** (ไม่ต้อง deep link หน้า — เข้าไปเหมือนปกติ) - ~~**การ์ดแต่ละไฟล์หุบ/กางดู bookmark+annotation ทั้งไฟล์** (คำขอผู้ใช้ 2026-09-23 — แถบ summary เป็น en: "Annotations & Bookmarks (N)")~~ → **แทนด้วย explorer 2 คอลัมน์ (redesign 2026-09-24 ตามคำขอผู้ใช้): ไม่มี navbar** (ถอด `AppNav` — เหลือ back link + หัวกลุ่ม + หมายเหตุ): - **ซ้าย = แผงตัวกรองเป็น tree menu 2 ระดับสไตล์ Quasar docs** (`q-tree--dense q-tree--no-connectors` — รอบสอง 2026-09-24 ตามคำขอผู้ใช้, ธีม Genesis พื้นสว่างตามคำยืนยัน) — desktop aside sticky 15rem scroll ภายใน; มือถือ `` "ตัวกรอง (N)" pattern เดียวกับหน้าสรุป (nested details ทำงานได้): 4 กลุ่ม (**ไฟล์** — "ทุกไฟล์" + รายไฟล์พร้อมไอคอนชนิดไฟล์ + จำนวน แม้ 0 เพื่อกด "เปิดเอกสารนี้" ได้ / **ชนิดรายการ** Bookmark+Annotation — checkbox คู่ **ไม่เลือก = แสดงทั้งหมด (default) ติ๊กอะไรเห็นเฉพาะสิ่งนั้น** (direct selection ตามคำขอผู้ใช้ 2026-09-24 รอบสอง — แบบแรกเปิดครบทั้งคู่ต้องปิดเอง = สับสน; เคยย้ายไป segmented บนหัว content แล้วย้ายกลับมาอยู่ในแผงตามคำขอผู้ใช้) / **ชนิดของ Annotation** 7 ค่า label TYPE_SHORT — กติกาเดียวกับชนิดรายการ: **ไม่เลือก = ทุกชนิด (default) ติ๊กอะไรเห็นเฉพาะชนิดนั้น** (คำขอผู้ใช้ 2026-09-24), **ติ๊กชนิดแล้วเลือก Annotation ในชนิดรายการให้อัตโนมัติ / เลิกเลือก Annotation แล้วล้างชนิดที่ติ๊กค้างไว้เริ่มใหม่ตาม default (คำขอผู้ใช้ 2026-09-24)**, แสดงเฉพาะชนิดที่มี ≥1 รายการ, HIGHLIGHT รวม 4 สไตล์เป็นค่าเดียว / **ผู้บันทึก** — "ทุกคน" + รายคน label จาก `userName || "ไม่ทราบชื่อ"` เรียง localeCompare th) = parent `
` หุบ/กางอิสระ + chevron หมุน 90° (pattern `bm-toggle` ของต้นไม้ bookmark N2), leaf = `