"use client"; import { useMemo, useState } from "react"; import { ChevronDown, FileStack } from "lucide-react"; import { FacetFilterPanel, createDefaultFacets, type ExplorerItem, type FacetFilterState, } from "@/components/explorer/FacetFilterPanel"; import type { AnnotationType } from "@/db/schema"; import { SummaryList } from "./SummaryList"; // หน้าสรุปหมายเหตุ (F15/D2) redesign 2026-09-24 ตามคำขอผู้ใช้ — explorer 2 คอลัมน์ // แบบหน้ากลุ่ม N9 โดยมิติแรกของตัวกรองเป็น "หน้า" แทน "ไฟล์" (ไฟล์เดียวทั้งหน้า): // ซ้าย = แผงตัวกรองกลาง FacetFilterPanel (หน้า/ชนิดรายการ/ชนิดของ Annotation/ // ผู้บันทึก) ขวา = SummaryList การ์ดรายหน้า — ข้อมูล query ฝั่ง server ส่งมา // ครั้งเดียวแล้วกรองใน memory (read-only snapshot ไม่มี SSE เหมือนเดิม) export function SummaryExplorer({ fileId, items, }: { fileId: string; items: ExplorerItem[]; }) { const [filters, setFilters] = useState(createDefaultFacets); // facet count เป็น static จากข้อมูลทั้งไฟล์ (ไม่หดตามตัวกรองอื่น — เหตุผลเดียว // กับหน้ากลุ่ม) const counts = useMemo(() => { let bm = 0; let ann = 0; const type: Partial> = {}; const user: Record = {}; for (const it of items) { user[it.dto.userId] = (user[it.dto.userId] ?? 0) + 1; if (it.kind === "bm") bm++; else { ann++; type[it.dto.type] = (type[it.dto.type] ?? 0) + 1; } } return { total: items.length, bm, ann, type, user }; }, [items]); // รายชื่อผู้บันทึกที่มีรายการจริง เรียงชื่อไทย — แทนบล็อก "ผู้ร่วมทำ" แบบอ่านอย่าง // เดียวของหน้าเดิม (กรองตามคนได้ + เห็น count) — localeCompare "th" รันทั้ง // SSR/hydration หลังย้ายมา client: CLDR สมัยใหม่ของ Node/browser ใกล้กันพอ // รับความเสี่ยง mismatch ต่ำไว้ (code review 2026-09-24 — ถ้าเจอ hydration // warning จริง ให้ย้าย sort นี้ไปฝั่ง server แล้วส่งเป็น prop) const users = useMemo(() => { const labelById = new Map(); for (const it of items) { if (!labelById.has(it.dto.userId)) { labelById.set(it.dto.userId, it.dto.userName || "ไม่ทราบชื่อ"); } } const out: { userId: string; label: string }[] = []; labelById.forEach((label, userId) => out.push({ userId, label })); return out.sort((a, b) => a.label.localeCompare(b.label, "th")); }, [items]); // มิติ "หน้า": เฉพาะหน้าที่มีรายการ ≥1 เรียงน้อย→มาก (ทางเข้าหน้าแทนรายการตาม // หน้าจุดไข่ปลาของหน้าเดิม — เห็น count ก่อนกระโดดดีกว่า) — ไอคอนเป็น spacer // ให้เลขชิดซ้ายตรงแนวกลุ่มผู้บันทึก; หน้าเยอะเป็นสิบ ๆ รายการใช้ optionsScroll // ให้ scroll ภายในกลุ่ม (max-h-64) กันกลุ่มอื่นถูกดันออกนอกจอ const pageOptions = useMemo(() => { const perPage = new Map(); for (const it of items) { perPage.set(it.pageIndex, (perPage.get(it.pageIndex) ?? 0) + 1); } return Array.from(perPage.entries()) .sort((a, b) => a[0] - b[0]) .map(([pi, n]) => ({ key: pi, label: `หน้า ${pi + 1}`, count: n, icon: , })); }, [items]); const firstGroup = { label: "หน้า", parentIcon: ( ), allLabel: "ทุกหน้า", options: pageOptions, optionsScroll: true, }; // pure predicate — ห้าม sort ซ้ำ (server เรียงลำดับอ่านให้แล้ว) const filtered = useMemo( () => items.filter( (it) => (filters.first === null || it.pageIndex === filters.first) && (filters.kinds.size === 0 || filters.kinds.has(it.kind)) && (it.kind === "ann" ? filters.types.size === 0 || filters.types.has(it.dto.type) : true) && (filters.userId === null || it.dto.userId === filters.userId), ), [items, filters], ); // จำนวนตัวกรองที่ ≠ default — โชว์ที่ summary มือถือ (กัน "กรองเก่ากรองเงียบ") // และเป็นเงื่อนไขปุ่ม "ล้างตัวกรอง"/ชุดพิมพ์ฉบับเต็ม const activeCount = (filters.first !== null ? 1 : 0) + (filters.kinds.size > 0 ? 1 : 0) + (filters.types.size > 0 ? 1 : 0) + (filters.userId !== null ? 1 : 0); const resetFilters = () => setFilters(createDefaultFacets()); return (
{/* มือถือ (< lg): ตัวกรองหุบ/กางเป็น
— named group `group/filters` กัน `group-open:` match ancestor ซ้ำชื่อ (pattern ของหน้ากลุ่ม); ตอนพิมพ์ ซ่อนทั้งสอง shell — กระดาษได้ฉบับเต็มจาก SummaryList เสมอ */}
ตัวกรอง{activeCount > 0 ? ` (${activeCount})` : ""}
{/* desktop: แผงตัวกรอง sticky — หน้าไม่มี navbar จึง top-6 พอ (เหมือนหน้ากลุ่ม) */} {/* div ไม่ใช่ section — กฎ print `main.summary-page section` (break-inside avoid-page) มุ่งที่การ์ดรายหน้าข้างใน ห้ามครอบทั้ง list กลายเป็นก้อนเดียว */}
); }