"use client"; import { useMemo, useState } from "react"; import { ChevronDown, FileImage, FileText, Folder } from "lucide-react"; import { FacetFilterPanel, createDefaultFacets, type ExplorerItem, type FacetFilterState, } from "@/components/explorer/FacetFilterPanel"; import type { AnnotationType } from "@/db/schema"; import { GroupList } from "./GroupList"; // N9 หน้ากลุ่ม (redesign 2026-09-24 ตามคำขอผู้ใช้): explorer 2 คอลัมน์ — ซ้ายเป็น // ตัวกรอง (ไฟล์/ชนิดรายการ/ชนิด annotation/ผู้บันทึก) ขวาเป็น list เดียวรวม // bookmark+annotation ทั้งกลุ่มไม่แยกไฟล์ — ข้อมูลทั้งหมด query ฝั่ง server ส่งมา // ครั้งเดียวแล้วกรองใน memory (read-only snapshot เหมือนเดิม ไม่มี SSE) // แผงตัวกรองเป็น component กลาง `components/explorer/FacetFilterPanel` (2026-09-24 // — เดิม FilterControls เป็นของหน้านี้) ใช้ร่วมกับหน้าสรุปที่มิติแรกเป็น "หน้า" /** ไฟล์ในกลุ่มแบบย่อ — เอาเฉพาะ field ที่ filter/list ใช้ (ไม่ส่ง row เต็มข้าม wire) */ export type GroupFileSummary = { id: string; title: string; isImage: boolean; }; /** รายการเดียวรวมทั้งกลุ่ม — server เรียงตามลำดับอ่านเอกสารให้แล้ว (ไฟล์ตาม * createdAt → หน้า → หน้าเดียวกัน bookmark ก่อน annotation → createdAt) */ export type GroupListItem = ExplorerItem; export function GroupExplorer({ files, items, }: { files: GroupFileSummary[]; items: GroupListItem[]; }) { const [filters, setFilters] = useState(createDefaultFacets); // facet count เป็น static จากข้อมูลทั้งกลุ่ม (ไม่หดตามตัวกรองอื่น — กันเข้าใจ // ผิดว่าตัวเลือกหายไปตอนไล่กรอง) const counts = useMemo(() => { let bm = 0; let ann = 0; const file: Record = {}; const type: Partial> = {}; const user: Record = {}; for (const it of items) { file[it.dto.fileId] = (file[it.dto.fileId] ?? 0) + 1; 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, file, type, user }; }, [items]); // รายชื่อผู้บันทึกที่มีรายการจริง เรียงชื่อไทย (คนละไฟล์รวมเป็นคนเดียว) 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]); // มิติแรกของหน้ากลุ่ม = ไฟล์ (count รายไฟล์ฝังใน options ของ firstGroup) const firstGroup = { label: "ไฟล์", parentIcon: , allLabel: "ทุกไฟล์", options: files.map((f) => ({ key: f.id, label: f.title, count: counts.file[f.id] ?? 0, icon: f.isImage ? ( ) : ( ), })), }; const filtered = useMemo( () => items.filter( (it) => (!filters.first || it.dto.fileId === 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 || it.dto.userId === filters.userId), ), [items, filters], ); const selectedFile = filters.first ? files.find((f) => f.id === filters.first) ?? null : null; // จำนวนตัวกรองที่ ≠ 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): ตัวกรองหุบ/กางเป็น
แบบเดียวกับหน้าสรุป — จอ lg ถูก display:none เอาออกจาก grid — ใช้ named group `group/filters` ไม่ใช่ `group` เปล่า: `group-open:` ของ parent tree ข้างใน match ทุก ancestor ที่มี .group[open] ใช้ชื่อซ้ำแล้ว chevron ของทุกกลุ่มหมุนตลอด ตอนกางแผงนี้ (responsive review 2026-09-24) */}
ตัวกรอง{activeCount > 0 ? ` (${activeCount})` : ""} {/* cap 70dvh + scroll ภายใน: กลุ่ม "ไฟล์"/"ผู้บันทึก" เยอะ ๆ ต้องไม่ดัน "พบ N รายการ"/list ลงไกลจนมองไม่เห็น (responsive review 2026-09-24) */}
{/* desktop: แผงตัวกรอง sticky — หน้าไม่มี navbar จึง top-6 พอ ไฟล์เยอะ scroll ภายในแผงเอง (p-3 กระชับตามความ dense ของ tree — design 2026-09-24) — h2 "ตัวกรอง (N)" ให้ parity กับ summary มือถือ, scroll-pt/pb กัน focus ring โดนขอบ scroll ตัด, max-h เป็น dvh ตาม pattern h-dvh ของ viewer (responsive review 2026-09-24) */}
); }