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

32 KiB
Raw Permalink Blame History

แผน 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/)

// 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 ยังทำงานเหมือนเดิม