eCourt-note/docs/PLAN-DOCSET.md
Your Name df502088b7 groups+summary: redesign หน้า /groups/[typeId] + หน้าสรุปเป็น explorer 2 คอลัมน์ (ตัวกรองซ้าย + list เดียว) — หน้าสรุปใช้มิติแรก "หน้า" ตามแบบหน้ากลุ่มที่ใช้ "ไฟล์" — (1) หน้า /groups/[typeId]: ถอด navbar (เหลือ back link — AppNav เหลือใช้ที่หน้า / อย่างเดียว) + ถอดหมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้", query ทั้งหมดฝั่ง server ส่ง GroupFileSummary[]+GroupListItem[] ให้ GroupExplorer กรองใน memory — ตัวกรอง tree menu 2 ระดับสไตล์ Quasar docs 4 กลุ่ม (ไฟล์/ชนิดรายการ/ชนิดของ Annotation/ผู้บันทึก — ชนิดรายการเป็น checkbox คู่ ไม่เลือก = แสดงทั้งหมด, ติ๊กชนิด → เลือก Annotation อัตโนมัติ, เลิกเลือก Annotation → ล้างชนิดค้าง) + GroupList แถว passive จุดคลิกเดียวคือปุ่ม "View" (2) แยกแผงตัวกรองเป็น component กลาง components/explorer/FacetFilterPanel.tsx — มิติแรกส่งเข้าทาง prop firstGroup (groups = ไฟล์ / สรุป = หน้า พร้อม optionsScroll ให้ scroll ภายในกลุ่ม max-h-64 กันหลายสิบหน้าดันกลุ่มอื่นออกจอ) + TYPE_SHORT/TYPE_ORDER รวมจาก 2 ที่ที่เคย define ซ้ำมาอยู่ utils/annotation-display.ts จุดเดียว (3) หน้าสรุป /annotate/[id]/summary: server ส่ง ExplorerItem[] (หน้า → bookmark ก่อน annotation → ในหน้าเดียวกัน bookmark เรียง yOffset ตาม N5/annotation ตาม createdAt) ให้ SummaryExplorer + SummaryList การ์ดรายหน้าหัวการ์ด sticky (เลขหน้า + บรรทัดสรุปหน้าคำนวณจากรายการที่เห็นจริงหลังกรอง + ปุ่มเปิดหน้านี้) + แถว passive ปุ่ม View deep link ?ann=/?page= เหมือนหน้ากลุ่ม — ถอดรายการตามหน้าจุดไข่ปลา + การ์ด folio + CollapsibleBlock ออก (anchor #page-N ของ section คงไว้ให้ลิงก์เก่ายังใช้ได้) (4) ตอนพิมพ์แสดงฉบับเต็มเสมอ — ตัวกรองที่ตั้งค้างต้องไม่ตัดรายการออกเงียบ ๆ บนกระดาษ (ตอนกรองอยู่ render ชุดบนจอ print:hidden + ชุดเต็ม hidden print:block, prop anchorIds กัน duplicate id) + กฎ print ใหม่ใน globals.css (section>ul>li break-inside:avoid-page + .page-head position:static) (5) ผ่าน code review 2026-09-24 (tsc/build/vitest 144 tests): คืน N5 yOffset ให้ bookmark ต่อหน้าที่ sort createdAt ทับไปโดยไม่ตั้งใจ + จดรับความเสี่ยง localeCompare "th" รันทั้ง SSR/hydration ไว้ที่ comment พร้อมทางแก้ + docs sync ครบ (CLAUDE.md โครงสร้างไฟล์ + PLAN-FEAT ถอดหมายเหตุ + PLAN-DOCSET อ้าง FacetFilterPanel) (คำขอผู้ใช้ 2026-09-24)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 17:34:23 +07:00

118 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# แผน 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 ภายใน; มือถือ `<lg` หุบเป็น `<details>` "ตัวกรอง (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 `<details open>` หุบ/กางอิสระ + chevron หมุน 90° (pattern `bm-toggle` ของต้นไม้ bookmark N2), leaf = `<button>` dense ไอคอน + ชื่อ + badge pill จำนวน — **เลือก button แทน label+input เพราะโดนกฎ coarse 44px ของ `main.listing-page` ใน globals.css พอดี** (desktop dense ~28px / จอสัมผัส 44px อัตโนมัติ — ไม่ต้องแต้ globals.css; แถวต้องมีลูกกลาง flex-1 กัน justify-content:center ของกฎ), active ผ่าน `aria-pressed:` (แถบซ้ายน้ำเงิน + `bg-primary/[0.08]` + ตัวน้ำเงิน, border-l โปร่งคงไว้ทุกแถวกัน layout shift — design review 2026-09-24 ถอด font-medium กันตัวอักษรกระตุกตอน toggle), multi-select (ชนิดรายการ/ชนิดของ Annotation) มี**กล่อง checkbox visual 14px หน้าไอคอน** (คำขอผู้ใช้ 2026-09-24) (เลือก = พื้นน้ำเงิน + ✓ ขาว — ยังเป็น visual ของ `<button aria-pressed>` ไม่ใช่ native input, กฎ coarse 44px ยังครบ; design review 2026-09-24: filter แบบ facet ใช้ checkbox convention ต่างจาก nav highlight เดี่ยวของ Quasar — เดิมรอบแรกเคยเป็น ✓ ขวามือ), **ไม่ได้ติ๊ก Annotation (ชนิดรายการ) = กลุ่มชนิด disabled + หุบ + หรี่ทั้งกลุ่ม** (คำขอผู้ใช้ 2026-09-24 — กรองชนิดมีความหมายเฉพาะเมื่อดู annotation; ติ๊ก Annotation แล้วกลุ่มกลับมาใช้ได้+กางให้อัตโนมัติ ผ่าน prop `open` ของ `<details>` ที่เขียน DOM เฉพาะตอนค่าเปลี่ยน การหุบ/กางด้วยมือจึงไม่ถูกเขียนทับ), ชนิด annotation reuse `AnnotationTypeIcon` ผ่าน `TYPE_ICON_DATA` (data ตัวแทนค่าว่าง), ผู้บันทึกไม่มีไอคอนใช้ spacer คง indentation, facet count เป็น static จากข้อมูลทั้งกลุ่ม (ไม่หดตามตัวกรองอื่น)
- **ขวา = list เดียวรวม bookmark+annotation ทั้งกลุ่มไม่แยกไฟล์** เรียงตามลำดับอ่านสำนวน: ไฟล์ตาม `createdAt` → หน้า → หน้าเดียวกันให้ที่คั่นหน้าก่อนหมายเหตุ (ไล่ตามตำแหน่งในเอกสาร) → `createdAt` tiebreak — sort ครั้งเดียวบน server ฝั่ง client กรองเป็น pure predicate; แต่ละแถวเป็น **passive surface — จุดคลิกเดียวคือปุ่ม `<Link>` "View"** ท้ายแถว (คำขอผู้ใช้ 2026-09-24: ไม่กดทั้งแถว — เดิมเป็น Link เต็มแถว) deep link เดียวกับหน้าสรุป F12 (`?ann=<id>` / `?page=N`) + `title`/`aria-label` บอกปลายทางรายแถว + รูปแบบแถว/ไอคอน/โน้ต/ป้าย "จัดการแล้ว"/"ใน: …" ของ sub bookmark ตามหน้าสรุป F15 + **ไฟล์ที่มาต้น meta line พร้อมไอคอนชนิดไฟล์ (truncate เฉพาะ span ของตัวเอง — ไม่กลืน หน้า/ผู้บันทึก/วันที่)** + วันเวลาที่สร้าง (`formatThaiDate` withTime) ในบรรทัด meta
- header ของคอลัมน์ content: "พบ N รายการ" + ปุ่ม **"เปิดเอกสารนี้"** (เฉพาะกรองไฟล์เดียว — ทางเข้าไฟล์ที่ไม่มีรายการใด ๆ, ตัดสินใจเก็บไว้ตามคำยืนยันผู้ใช้ 2026-09-24) + **"ล้างตัวกรอง"** (เฉพาะมีตัวกรอง active — ชนิดรายการที่ติ๊กไว้นับเป็น 1 ตัวกรอง)
- empty state 3 แบบ: กลุ่มไม่มีรายการเลย / กรองจนไม่เหลือ (+ ปุ่มล้าง) / เลือกไฟล์เดียวที่ไม่มีรายการ (+ ปุ่มเปิดเอกสารนี้)
- สถาปัตยกรรม: `page.tsx` query เต็ม 2 query (`inArray fileId` + `orderBy` หน้า) แล้วจัดเป็น `GroupFileSummary[]` + `GroupListItem[]` ส่งให้ **client component เดียว** `GroupExplorer` (state ตัวกรองก้อนเดียว + `useMemo` กรอง/นับ facet) แยก presentational เป็น `FacetFilterPanel` (แผงตัวกรองกลาง `components/explorer/` ใช้ร่วมกับหน้าสรุป — ย้ายจาก FilterControls 2026-09-24)/`GroupList` — read-only ล้วน ไม่มี Server Action/SSE ใหม่
- ไม่มี SSE (snapshot ณ ตอนโหลด เหมือนหน้าสรุป F15) ไม่มีปุ่มแก้ไข/ย้ายไฟล์ (จัดการที่ e-Court ฝั่งเดียว)
- `generateMetadata` ตั้งชื่อแท็บ browser ตามชื่อกลุ่ม (pattern เดียวกับหน้าสรุป F15)
### D4 — สิทธิ์/การเข้าถึง
- ใครก็ที่ login เห็นได้ทุกกลุ่ม (mock login — roles ยัง out of scope ตามแผนหลัก)
- ไม่มี Server Action ใหม่ → ไม่มี publish SSE / `revalidatePath` ของแผนนี้ (การเปลี่ยนกลุ่มเกิดที่ N8 ตอน register แล้ว redirect ออกจาก `/` ทันที — ไม่มี Router Cache ค้าง)
## Docs/rules sync (plan-first — ทำเป็น task แรก ก่อนแตะโค้ด)
| ไฟล์ | สิ่งที่แก้ |
|---|---|
| `docs/PLAN.md` §3 | เพิ่ม column `files.type_id` (text NULL + index — ไม่มีตารางกลุ่ม/ไม่มีคอลัมน์ชื่อกลุ่ม) + `files.external_id` (ค้างจาก N8 — ทำพร้อมกันตอน sync 2026-09-23) |
| `docs/PLAN-FEAT.md` §7 | หัวข้อใหม่ **N9 · กลุ่มเอกสารตาม typeId + หน้า /groups/[typeId]** (format ตาม N5–N7 + อ้างแผนฉบับเต็ม `docs/PLAN-DOCSET.md`) |
| `CLAUDE.md` | whitelist ฟีเจอร์เสริมหลังแผน เพิ่ม N9 + โครงสร้างไฟล์เพิ่ม `app/groups/[typeId]/page.tsx` |
| `.claude/rules/data-layer.md` | เพิ่ม: `files.type_id` เขียนที่ N8 `registerExternalFile` เท่านั้น (ลิงก์ไม่ส่ง = คงเดิม) — ไม่มี action/SSE ใหม่ของ N9 |
- `docs/requirement/gap-analysis.md` 5.3.10 ครึ่งหลัง ("หรือชุดของเอกสาร") — เดิมแผนนี้ไม่ปิด (แสดงเฉพาะ *เอกสาร* รวมกลุ่ม) แต่**ปิดแล้ว 2026-09-23** ด้วยการหุบ/กางดู bookmark+annotation ต่อไฟล์บนหน้ากลุ่ม (D3)
- `.claude/rules/realtime.md`, `pdf-viewer.md`, `coordinates.md` **ไม่แตะ**
## ลำดับ task + verify (ทุก task: `npx tsc --noEmit` ผ่าน)
| # | งาน | ไฟล์หลัก | Verify เพิ่มเติม |
|---|---|---|---|
| 1 | Docs sync N9 | ตารางด้านบน | สเปก §7 ครบตาม format N5–N7; PLAN.md §3 มี column ใหม่ |
| 2 | Schema + migrate — **ทำแล้วกับ N8** (`type_id` + `files_type_id_idx` อยู่ใน `db/schema.ts` + migration `drizzle/0002` idempotent + dev DB ยืนยันด้วย psql 2026-09-23) | `db/schema.ts`, `drizzle/` | ตรวจแล้วผ่าน: psql `\d files` เห็น `type_id` + index (ไม่มี type_name) |
| 3 | หน้า `/` จัดกลุ่ม | `app/page.tsx` | ทดสอบด้วยลิงก์จำลอง (การ์ด `EcourtLinkForm` / path `/samples/…` + `typeId` — N8 พร้อมแล้ว) หรือ update `files` set `type_id` ตรง ๆ ก็ได้: จัด section ถูก / "ไม่มีกลุ่ม" ท้ายสุด; **regression: อัปโหลด N4 + Annotate ยังทำงาน** |
| 4 | หน้า `/groups/[typeId]` | `app/groups/[typeId]/page.tsx` | รายการครบถูกลำดับ + หมายเหตุแสดง + นับ annotation/bookmark ตรง + กรองไฟล์เดียวแล้วกด "เปิดเอกสารนี้" เปิดไฟล์ได้ (redesign 2026-09-24 — เดิมปุ่ม Annotate ต่อการ์ด); typeId ไม่รู้จัก = 404; ชื่อแท็บ browser ตามกลุ่ม |
| 5 | ปิดงาน | — | plan-reviewer + code-reviewer + vitest + `tsc`; e2e เต็ม (ลิงก์ e-Court จริง) ทำตอนปิดงาน N8 |
## Edge cases (ออกแบบไว้ล่วงหน้า)
1. **ลิงก์รอบใหม่ส่ง `typeId` คนละค่า** → ย้ายกลุ่มตามค่าล่าสุด (เกิดที่ N8) — annotation/bookmark ผูกกับ `file_id` ไม่เกี่ยวกับกลุ่ม จึงตามไฟล์ไปเอง
2. **ลิงก์รอบใหม่ไม่ส่ง `typeId`** → ไฟล์คงกลุ่มเดิม (N8: ห้ามเคลียร์ค่าเดิม)
3. **ไฟล์ไม่เคยมี `typeId`** (seed / อัปโหลด N4 ที่ไม่กรอกกลุ่ม / ลิงก์เก่า) → section "ไม่มีกลุ่ม" บนหน้า `/`, ไม่มีหน้ากลุ่ม
4. **กลุ่มแสดงผลด้วยรหัสล้วน** — ไม่มีคอลัมน์ชื่อกลุ่ม (ถอด `type_name` ตามคำขอผู้ใช้ 2026-09-22) — ถ้าภายหลังต้องการชื่อกลุ่ม กลับมาเพิ่ม column ใหม่ตาม governance
5. **`typeId` มีอักขระพิเศษ/ยาว** → ผ่าน URL ด้วย `encodeURIComponent`; normalize ที่ N8 (trim → ว่าง = NULL, cap 200); Next decode ให้อัตโนมัติตอนอ่าน `params`
6. **กลุ่มว่างลง (ไฟล์ย้ายออกหมด)** → กลุ่มหายจากหน้า `/` เอง และ `/groups/[typeId]` กลายเป็น 404 (query จาก `files` จริง ไม่มีตารางกลุ่มค้าง)
7. **Dev DB แชร์กับการทดสอบของผู้ใช้** → เทสหน้า UI ด้วยการตั้ง `type_id` บน id ที่จับเป๊ะ ไม่ reset ข้อมูลทั้งกลุ่ม
8. **N8 ยังไม่ implement ตอนทำ N9** → ~~verify หน้า UI ด้วยการเขียน DB ตรง (task 3–4) แล้วรอ e2e จริงตอนปิดงาน N8~~ (ล้าสมัย — N8 implement ครบแล้ว 2026-09-22: verify ได้ด้วยลิงก์จำลอง/การ์ด `EcourtLinkForm` + path `/samples/…` ทันที)
9. **`typeId` มี `/` (เช่น เลขคดีไทย `ด2024/2567`)** → ลิงก์ `/groups/…%2F…` — Next 14 route ได้ (match บน raw path ก่อน decode) แต่ reverse proxy บางตัว (Tomcat/JBoss ปฏิเสธ `%2F` โดย default) อาจ 404 ทั้งที่กลุ่มมีจริง — **known risk ที่รับไว้** (code-reviewer 2026-09-23): ทดสอบ e2e กับ deployment จริงของ e-Court ตอนปิดงาน N8 ถ้าเจอปัญหาแก้ที่จุดสร้างลิงก์จุดเดียว (replace `/` ด้วยตัวอักขระปลอดภัยตอน encode) หรือเปลี่ยนเป็น query param
## Out of scope (ไม่ทำในแผนนี้)
- จัดกลุ่ม/แก้ชื่อ/ลบกลุ่มด้วยมือใน UI ของเรา (ทิ้งจากร่างเดิม — กลุ่มมาจาก e-Court อย่างเดียว)
- ไฟล์อยู่หลายกลุ่ม (many-to-many) / กลุ่มซ้อนหลายระดับ
- ~~bookmark รวมทั้งกลุ่ม~~ — **ทำแล้ว (2026-09-23)**: การ์ดไฟล์หน้ากลุ่มหุบ/กางดู bookmark+annotation ต่อไฟล์ (D3; รูปแบบเปลี่ยนเป็น list รวมกลุ่มละแถวบอกไฟล์ที่มา ใน redesign 2026-09-24)
- export JSON ตามกลุ่ม (N7 ยังรับ `fileId` เดียว)
- API ดึงข้อมูลให้ระบบภายนอก = **N10** `docs/PLAN-API.md` (แผนแยก)
- รายการเอกสารครบทุกฉบับของคดีรวมฉบับที่ยังไม่เคยเปิด (ข้อมูลอยู่ที่ e-Court)
## Verification (ภาพรวม end-to-end — หลัง N8 เสร็จ)
1. `npm run dev` → login → เปิดลิงก์ e-Court 2 ไฟล์ที่ `typeId` เดียวกัน + 1 ไฟล์ `typeId` ต่างกลุ่ม → หน้า `/` จัด 2 section ตามกลุ่ม + section "ไม่มีกลุ่ม" (ไฟล์ seed/อัปโหลดที่ไม่กรอกกลุ่ม); อัปโหลดไฟล์พร้อมกรอกกลุ่ม → การ์ดไปอยู่ section ของกลุ่มนั้น + หน้า `/groups/[typeId]` เห็นไฟล์อัปโหลดด้วย
2. หัวกลุ่ม → "เอกสารในกลุ่ม" → `/groups/[typeId]`: เห็นไฟล์ครบตามลำดับ (หมายเหตุถูกถอด 2026-09-24) + จำนวน annotation/bookmark ต่อไฟล์
3. หน้า `/groups/[typeId]` → เห็น list เดียวรวม bookmark+annotation ทั้งกลุ่ม (แถวบอกไฟล์ที่มา) + กดแถวแต่ละรายการเด้งไปตำแหน่งนั้นใน viewer (`?ann=` / `?page=`); ตัวกรอง 4 กลุ่มไล่ได้ครบ — เลือกไฟล์เดียว/เฉพาะ bookmark/เฉพาะชนิด annotation/ของคนใดคนหนึ่ง แล้วจำนวน "พบ N รายการ" ลดตาม + "ล้างตัวกรอง" กลับมาครบ (redesign 2026-09-24; เดิมเป็นการ์ดต่อไฟล์กาง `<details>` "Annotations & Bookmarks (N)")
4. กด Annotate → เข้า viewer ของไฟล์นั้นได้ตามปกติ
5. เปิดลิงก์รอบใหม่ของไฟล์เดิมด้วย `typeId` คนละค่า → ไฟล์ย้ายกลุ่ม กลุ่มเดิมลดจำนวน/หายไปเมื่อว่าง
6. Regression: อัปโหลด N4 / annotate / ค้นหา N6 / export JSON N7 ยังทำงานเหมือนเดิม