diff --git a/app/api/annotations/export/route.ts b/app/api/annotations/export/route.ts index 0b96dd9..50c579b 100644 --- a/app/api/annotations/export/route.ts +++ b/app/api/annotations/export/route.ts @@ -14,7 +14,7 @@ import { type PDFPage, type PDFRef, } from "pdf-lib"; -import { asc, eq, inArray } from "drizzle-orm"; +import { and, asc, eq, inArray } from "drizzle-orm"; import { db } from "@/db"; import { PdfPathError, readPublicPdf } from "@/lib/server-pdf"; @@ -274,9 +274,21 @@ export async function GET(req: Request) { .from(annotations) .where(eq(annotations.fileId, fileId)) .orderBy(asc(annotations.createdAt)); + // F4: export ตาม filter รายคนที่กำลังดู (?user= จาก sidebar — ไม่ส่ง = ทั้งหมด) + // REDACTION ติดไปเสมอแม้กรองคนอื่น (D4 — กล่องดำหายไป = เนื้อหาที่ปิดกั้นกลับมาอ่านได้) + const userParam = url.searchParams.get("user"); + const filteredRows = userParam + ? rows.filter((r) => r.userId === userParam || r.type === "REDACTION") + : rows; + // ชื่อผู้ใช้เติมท้ายชื่อไฟล์ (mirror ในตาราง — ไม่รับ text จาก client) — กรองแล้ว + // ผู้นั้นยังไม่มีรายการเลย = ใช้ userId ตรง ๆ + const filterName = userParam + ? (rows.find((r) => r.userId === userParam && r.userName)?.userName ?? + userParam) + : null; // sort แยกตามชนิดก่อนวาด (stable ภายในชนิดด้วย createdAt) — กัน REDACTION เก่าโดน // HIGHLIGHT/DRAW ที่สร้างทีหลังทับจนข้อความง้ำกลับมาอ่านได้ (D4) - const dtos = rows + const dtos = filteredRows .map(toAnnotationDTO) .sort((a, b) => TYPE_ORDER[a.type] - TYPE_ORDER[b.type]); @@ -634,10 +646,16 @@ export async function GET(req: Request) { // bookmarks → PDF outline (แผง Bookmarks ของ reader กดกระโดดไปหน้า + yOffset) // เรียงตามลำดับหน้าเหมือน sidebar (byPageOrder ของ N5: หน้า → ตำแหน่งบนหน้า → เวลาสร้าง) + // กรองตามรายคนเหมือน annotation (คำขอผู้ใช้ 2026-09-22) — หัวข้อแม่โดนกรองแต่ลูกยังอยู่ + // ลูกขึ้น root (buildOutline จัดการ parent หายเคสนี้อยู่แล้ว) const bookmarkRows = await db .select() .from(bookmarks) - .where(eq(bookmarks.fileId, fileId)) + .where( + userParam + ? and(eq(bookmarks.fileId, fileId), eq(bookmarks.userId, userParam)) + : eq(bookmarks.fileId, fileId), + ) .orderBy( asc(bookmarks.pageIndex), asc(bookmarks.yOffset), @@ -646,7 +664,9 @@ export async function GET(req: Request) { buildOutline(doc, bookmarkRows); const out = await doc.save(); - const filename = encodeRFC5987(`${file.title}-annotated.pdf`); + const filename = encodeRFC5987( + `${file.title}-annotated${filterName ? `-${filterName}` : ""}.pdf`, + ); return new Response(new Uint8Array(out), { headers: { "content-type": "application/pdf", diff --git a/components/pdf/PDFViewer.tsx b/components/pdf/PDFViewer.tsx index 7c25842..a561b5a 100644 --- a/components/pdf/PDFViewer.tsx +++ b/components/pdf/PDFViewer.tsx @@ -1322,11 +1322,15 @@ export default function PDFViewer({ return Array.from(seen, ([userId, userName]) => ({ userId, userName })); }, [optimisticAnnotations, user]); - // F4: กรองฝั่ง client ล้วน — ไม่แตะข้อมูลใน DB (A3) + // F4: กรองฝั่ง client ล้วน — ไม่แตะข้อมูลใน DB (A3) — REDACTION คงไว้เสมอแม้ + // กรองดูของคนอื่น (D4 สูตรเดียวกับ export: กล่องดำหายจากจอ = เนื้อหาที่ปิดกั้น + // กลับมาอ่านได้ และจอจะไม่ตรงกับไฟล์ export ที่คงกล่องไว้) const visibleAnnotations = useMemo(() => { if (userFilter === "all") return optimisticAnnotations; const target = userFilter === "mine" ? user?.userId : userFilter; - return optimisticAnnotations.filter((a) => a.userId === target); + return optimisticAnnotations.filter( + (a) => a.userId === target || a.type === "REDACTION", + ); }, [optimisticAnnotations, userFilter, user]); // FIX-8: bbox (หน่วย %) ของรายการที่ hover ตรวจได้ — เดิน paths @@ -1490,7 +1494,19 @@ export default function PDFViewer({ setExporting(true); setExportError(null); try { - const res = await fetch(withBasePath(`/api/annotations/export?fileId=${file.id}`)); + // F4: export ตาม filter รายคนที่กำลังดู ("mine" resolve เป็น userId จริงก่อนส่ง — + // route กรองฝั่ง server + เติมชื่อผู้ใช้ท้ายชื่อไฟล์, REDACTION ติดไปเสมอ) + const filterUser = + userFilter === "all" + ? null + : userFilter === "mine" + ? (user?.userId ?? null) + : userFilter; + const res = await fetch( + withBasePath( + `/api/annotations/export?fileId=${file.id}${filterUser ? `&user=${encodeURIComponent(filterUser)}` : ""}`, + ), + ); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(text || `HTTP ${res.status}`); diff --git a/components/sidebar/AnnotationSidebar.tsx b/components/sidebar/AnnotationSidebar.tsx index 1ed2317..324b4ac 100644 --- a/components/sidebar/AnnotationSidebar.tsx +++ b/components/sidebar/AnnotationSidebar.tsx @@ -374,13 +374,14 @@ export default function AnnotationSidebar({ } /** ผลการค้นหา F14 หลังกรองตามเจ้าของหมายเหตุ (F4) ฝั่ง client — searchAnnotations - * ไม่รับ userId และห้ามแต้ server action: สูตรเดียวกับ visibleAnnotations ของ - * PDFViewer — เปลี่ยนตัวกรองหลังค้นแล้ว ผลกรองทันทีไม่ต้องค้นใหม่ */ + * ไม่รับ userId และห้ามแต้ server action: สูตรเดียวกันกับ visibleAnnotations ของ + * PDFViewer (รวม REDACTION ของทุกคนไว้เสมอ — D4) — เปลี่ยนตัวกรองหลังค้นแล้ว + * ผลกรองทันทีไม่ต้องค้นใหม่ */ const visibleSearchResults = useMemo(() => { if (searchResults === null) return null; if (userFilter === "all") return searchResults; const target = userFilter === "mine" ? userId : userFilter; - return searchResults.filter((a) => a.userId === target); + return searchResults.filter((a) => a.userId === target || a.type === "REDACTION"); }, [searchResults, userFilter, userId]); // ─── F19/A7: ประวัติ/กู้คืน — dialog ต่อรายการ (แสดงเฉพาะรายการของตัวเอง) ───── @@ -435,9 +436,25 @@ export default function AnnotationSidebar({ const pendingItems = annotations.filter((a) => a.type !== "COMMENT" || !a.resolved); const resolvedItems = annotations.filter((a) => a.type === "COMMENT" && a.resolved); + // F4: bookmarks ที่แสดง กรองตามเจ้าของหมายเหตุเหมือน annotation (คำขอผู้ใช้ + // 2026-09-22 ให้ตรงกับ export ที่กรอง PDF outline ตามผู้บันทึก) — หัวข้อแม่โดนกรอง + // แต่ลูกยังอยู่ ลูกขึ้น root (พฤติกรรมเดียวกับ buildOutline ของ export) — ใช้เฉพาะจุด + // แสดงผล (ต้นไม้/กลุ่มหน้า/ตัวเลขแท็บ): popup ย้ายหัวข้อ (movableTo) กับ bookmarkById + // ยังเห็นต้นไม้เต็ม ย้ายไปใต้หัวข้อของคนอื่นได้ตามเดิม + const keptBookmarks = + userFilter === "all" + ? bookmarks + : bookmarks.filter( + (b) => b.userId === (userFilter === "mine" ? userId : userFilter), + ); + const keptIds = new Set(keptBookmarks.map((b) => b.id)); + const visibleBookmarks = keptBookmarks.map((b) => + b.parentId && !keptIds.has(b.parentId) ? { ...b, parentId: null } : b, + ); + // มุมมอง "ตามหน้า": จัดกลุ่มตามหน้า เรียงน้อย→มาก — bookmark เอาเฉพาะ root // (sub ดูในต้นไม้ "ตามหัวข้อ") / annotation เก็บลำดับเดิมตามเวลาสร้าง - const bmRootPages = groupByPage(bookmarks.filter((b) => !b.parentId).sort(byPageOrder)); + const bmRootPages = groupByPage(visibleBookmarks.filter((b) => !b.parentId).sort(byPageOrder)); const annPages = groupByPage(annotations); // F12/C4: คัดลอก deep link ไปยัง annotation นี้ (?page= + ?ann=) @@ -715,7 +732,7 @@ export default function AnnotationSidebar({ } const renderBookmarkNode = (b: BookmarkDTO, depth: number) => { - const children = bookmarks.filter((x) => x.parentId === b.id).sort(byPageOrder); + const children = visibleBookmarks.filter((x) => x.parentId === b.id).sort(byPageOrder); const hasChildren = children.length > 0; const collapsed = collapsedIds.has(b.id); const editing = editBookmark?.id === b.id; @@ -1027,7 +1044,7 @@ export default function AnnotationSidebar({
{( [ - ["bookmarks", `Bookmark (${bookmarks.length})`], + ["bookmarks", `Bookmark (${visibleBookmarks.length})`], ["annotations", `Annotation (${annotations.length})`], ] as const ).map(([key, label]) => ( @@ -1318,6 +1335,10 @@ export default function AnnotationSidebar({

ยังไม่มีที่คั่นหน้า — กดปุ่มคั่นหน้าบนแถบเครื่องมือเพื่อเพิ่ม

+ ) : visibleBookmarks.length === 0 ? ( +

+ ไม่มีที่คั่นหน้าของเจ้าของหมายเหตุที่เลือก +

) : ( <> {/* สลับมุมมองแท็บนี้: ต้นไม้หัวข้อ ↔ root แยกตามหน้า */} @@ -1345,7 +1366,7 @@ export default function AnnotationSidebar({ {!collapsed && (