import type { Metadata } from "next"; import Link from "next/link"; import { asc, eq } from "drizzle-orm"; import { notFound } from "next/navigation"; import { ArrowLeft, ClipboardList } from "lucide-react"; import type { ExplorerItem } from "@/components/explorer/FacetFilterPanel"; import { SummaryExplorer } from "@/components/summary/SummaryExplorer"; import { db } from "@/db"; import { annotations, bookmarks, files, toAnnotationDTO, toBookmarkDTO, } from "@/db/schema"; // หน้าสรุปหมายเหตุ (F15/D2) — server component รวม annotation (ทุกผู้ใช้) + bookmark // แต่ละแถวกระโดดเป็น deep link `/note/annotate/?ann=` / `?page=` (F12) // — design "แฟ้มสำนวน" 2026-09-17 (แถบหัวน้ำเงิน + การ์ดรายหน้าขอบ folio + // รายการตามหน้าจุดไข่ปลา) → redesign 2026-09-24 ตามคำขอผู้ใช้เป็น explorer // 2 คอลัมน์แบบหน้ากลุ่ม N9: ตัวกรองฝั่งซ้าย (หน้า/ชนิดรายการ/ชนิดของ // Annotation/ผู้บันทึก) + การ์ดรายหน้าหัว sticky — แถบหัวน้ำเงิน + action bar + // empty state คงเดิม; query ทั้งหมดทำที่นี่แล้วส่ง `ExplorerItem[]` ให้ // SummaryExplorer กรองใน memory ฝั่ง client (read-only snapshot ไม่มี SSE) const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** focus ring ร่วมของทุกลิงก์ในหน้า — Genesis .focus-ring (outline น้ำเงินทึบ 2px * + offset ตาม spec Elevation); จอสัมผัสได้ 44px อัตโนมัติจากกฎ * `main.summary-page a`/`button` ใน globals.css (ห้ามถอด class summary-page ของ
) */ const RING_CLS = "focus-ring"; export const dynamic = "force-dynamic"; // design review 2026-09-17 M5: ชื่อแท็บ browser ระบุคดี — เลขานุการเปิดหลายคดี // หลายแท็บพร้อมกัน (เดิมทุกแท็บชื่อ "Judge's Note" เหมือนกันหมดแยกไม่ออก) export async function generateMetadata({ params, }: { params: { id: string }; }): Promise { const { id } = params; if (!UUID_RE.test(id)) return { title: "สรุปหมายเหตุ | Judge's Note" }; const [file] = await db .select({ title: files.title }) .from(files) .where(eq(files.id, id)); return { title: `สรุปหมายเหตุ — ${file?.title ?? "ไม่พบไฟล์"} | Judge's Note`, }; } export default async function SummaryPage({ params, }: { params: { id: string }; }) { const { id } = params; if (!UUID_RE.test(id)) notFound(); const [file] = await db.select().from(files).where(eq(files.id, id)); if (!file) notFound(); const annRows = await db .select() .from(annotations) .where(eq(annotations.fileId, id)) .orderBy(asc(annotations.pageIndex), asc(annotations.createdAt)); const dtos = annRows.map(toAnnotationDTO); // bookmark เรียงตามหน้า → ตำแหน่งบนหน้า → เวลาสร้าง (N5 เหมือน sidebar) const bmRows = await db .select() .from(bookmarks) .where(eq(bookmarks.fileId, id)) .orderBy( asc(bookmarks.pageIndex), asc(bookmarks.yOffset), asc(bookmarks.createdAt), ); const bmDtos = bmRows.map(toBookmarkDTO); const bookmarkById = new Map(bmDtos.map((b) => [b.id, b] as const)); // list เดียวรวมทั้งไฟล์ไล่ตามลำดับอ่าน: หน้า → หน้าเดียวกันให้ที่คั่นหน้าก่อน // หมายเหตุ → ในหน้าเดียวกัน bookmark เรียงตามตำแหน่งบนหน้า (N5 เหมือน query/ // sidebar — code review 2026-09-24 คืนจาก createdAt ที่เคยแทนที่โดยไม่ตั้งใจ) // ส่วน annotation ตาม createdAt (sort ครั้งเดียวบน server — ฝั่ง client กรอง // เป็น pure predicate ไม่ sort ซ้ำ) const items: ExplorerItem[] = [ ...bmDtos.map((dto) => ({ kind: "bm" as const, pageIndex: dto.pageIndex, dto, parentTitle: dto.parentId ? bookmarkById.get(dto.parentId)?.title ?? null : null, })), ...dtos.map((dto) => ({ kind: "ann" as const, pageIndex: dto.pageIndex, dto })), ].sort( (a, x) => a.pageIndex - x.pageIndex || (a.kind === x.kind ? 0 : a.kind === "bm" ? -1 : 1) || (a.kind === "bm" && x.kind === "bm" ? a.dto.yOffset - x.dto.yOffset : new Date(a.dto.createdAt).getTime() - new Date(x.dto.createdAt).getTime()), ); // จำนวนหน้าที่มีรายการ ≥1 สำหรับสถิติแถบหัว const pageCount = new Set(items.map((it) => it.pageIndex)).size; return (
{/* หัวกระดาษสำนวน — แถบน้ำเงินสืบต่อจาก tile JN ของแบรนด์ (หน้ารายการ/login): ชื่อรายงาน + ชื่อแฟ้ม + สถิติในที่เดียว แล้วปิดท้ายด้วยเส้นคั่นบาง (ตอนพิมพ์เป็นกรอบเส้นน้ำเงินตามกฎ @media print ของ summary-page) */}

สรุปหมายเหตุ

{file.title}

{items.length > 0 && (

Annotation {dtos.length} รายการ · Bookmark {bmDtos.length} รายการ ·{" "} {pageCount} หน้า

)}
{/* แถบ action — ghost button ขาวมีขอบ ภาษาเดียวกับการ์ดของหน้า (คำขอผู้ใช้ 2026-09-17 แทนลิงก์ข้อความเปล่า — เห็นเป็นปุ่มชัดขึ้น; จอสัมผัสได้ 44px จากกฎ coarse; ตอนพิมพ์ซ่อนทิ้ง — ปุ่มกลับไม่มีความหมายบนกระดาษ) ปุ่มส่งออก CSV (F16) / Bookmark JSON (N7) ถูกถอด 2026-09-25 ตามคำขอผู้ใช้ — ระบบภายนอกดึงข้อมูลผ่าน N10 API แทนทั้งหมดแล้ว */}
กลับหน้าอ่าน
{items.length === 0 ? ( /* ไฟล์ว่าง = ไม่ mount explorer เลย — แผงกรองของข้อมูลว่างไม่มีความหมาย (minimize choices) ทางเข้าเอกสารคือปุ่ม "เปิดหน้าอ่าน" */

ยังไม่มี Annotation หรือ Bookmark ในไฟล์นี้

เปิดหน้าอ่านเพื่อมาร์กข้อความ วาดเส้น ใส่คอมเมนต์ หรือคั่นหน้า

เปิดหน้าอ่าน
) : ( )}
); }