2026-09-17 19:48:58 +07:00
|
|
|
import type { Metadata } from "next";
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
import Link from "next/link";
|
|
|
|
|
import { asc, eq } from "drizzle-orm";
|
|
|
|
|
import { notFound } from "next/navigation";
|
2026-09-17 19:48:58 +07:00
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
Bookmark,
|
|
|
|
|
Braces,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
ClipboardList,
|
|
|
|
|
FileDown,
|
|
|
|
|
StickyNote,
|
|
|
|
|
} from "lucide-react";
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
|
2026-09-17 19:48:58 +07:00
|
|
|
import { AnnotationTypeIcon } from "@/components/AnnotationTypeIcon";
|
|
|
|
|
import CollapsibleBlock from "@/components/summary/CollapsibleBlock";
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
import { db } from "@/db";
|
2026-09-17 19:48:58 +07:00
|
|
|
import {
|
|
|
|
|
annotations,
|
|
|
|
|
bookmarks,
|
|
|
|
|
files,
|
|
|
|
|
toAnnotationDTO,
|
|
|
|
|
toBookmarkDTO,
|
|
|
|
|
type AnnotationDTO,
|
|
|
|
|
type AnnotationType,
|
|
|
|
|
type BookmarkDTO,
|
|
|
|
|
} from "@/db/schema";
|
|
|
|
|
import {
|
|
|
|
|
annotationNote,
|
|
|
|
|
annotationPreviewText,
|
|
|
|
|
annotationTypeLabel,
|
|
|
|
|
} from "@/utils/annotation-display";
|
BASE_PATH: serve ใต้ /docedit/note — next.config.mjs ตั้ง basePath + env NEXT_PUBLIC_BASE_PATH (ประกาศค่าเดียวที่ไฟล์นี้) ให้ utils/base-path.ts (ใหม่) withBasePath() เติม prefix ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (fetch/EventSource/plain href/workerSrc/img src): PDFViewer (workerSrc, /api/pdf ซ่อม ToUnicode, fallback file.pdfUrl, image branch N3 รวบเป็นตัวแปร fileUrl, export PDF F8), SSE stream ใน useAnnotationRealtime, ปุ่มส่งออก bookmark JSON (N7) + คัดลอกลิงก์ F12 ใน AnnotationSidebar (window.location.origin + basePath กัน paste แล้ว 404), Export CSV (F16) หน้า summary — <Link>/next/navigation/middleware.ts ไม่ต้องแก้ (Next 14.2 auto-prefix ให้ + matcher จับ path ที่ตัด basePath แล้ว + NextURL redirect ใส่กลับเอง ยืนยันจาก source ใน node_modules) + pdf_url ใน DB เก็บ path ไม่รวม basePath เติมตอนใช้เท่านั้น + sync เอกสารตาม governance: CLAUDE.md invariant #6 (workerSrc ผ่าน withBasePath) + #12 ใหม่ (URL ฝั่ง browser ที่สร้างเองต้องผ่าน withBasePath), PLAN.md A3/§4, rules pdf-viewer/realtime, plan-reviewer #8, README URL localhost:3000/docedit/note — verify: tsc strict ผ่าน + curl (root / 404, ไม่มี cookie 307→/docedit/note/login, worker 200, SSE reachable, มี cookie 200) + Playwright login จริงเปิดไฟล์ 44 หน้า render ครบ (SSE /api/pdf /worker 200 ผ่าน basePath) + code-reviewer ผ่าน (vitest 126 tests, ไม่พบ bug, blob URL ไม่โดน prefix ซ้ำ, env inline ครบทั้ง client/server ตาม define-env-plugin) — deploy: basePath เป็น build-time ต้อง build image ใหม่ถึงจะเสิร์ฟที่ /docedit/note + reverse proxy ต้อง forward path เต็มแบบไม่ strip prefix
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 16:06:20 +07:00
|
|
|
import { withBasePath } from "@/utils/base-path";
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
|
2026-09-17 19:48:58 +07:00
|
|
|
// หน้าสรุปหมายเหตุ (F15/D2) — server component รวม annotation (ทุกผู้ใช้) + bookmark
|
|
|
|
|
// จัดกลุ่มตามหน้า แต่ละแถวกระโดดเป็น deep link `/annotate/<id>?ann=` / `?page=` (F12)
|
|
|
|
|
// — design "แฟ้มสำนวน" 2026-09-17: แถบหัวกระดาษน้ำเงิน (สืบต่อจาก tile JN ของแบรนด์) +
|
2026-09-18 12:09:43 +07:00
|
|
|
// รายการตามหน้าจุดไข่ปลา + การ์ดรายหน้า 1 หน้า 1 การ์ด (คำขอผู้ใช้ 2026-09-17 แทนแผ่น
|
|
|
|
|
// กระดาษแผ่นเดียวคั่นเส้น) แต่ละการ์ดมีขอบ folio เลขหน้าใหญ่; รายการตามหน้าเป็น anchor
|
2026-09-17 19:48:58 +07:00
|
|
|
// link (#page-N) ล้วน — desktop เป็น sticky aside แบบการ์ด ส่วนมือถือ (< lg) เป็น
|
|
|
|
|
// <details> แบบการ์ด ไม่มี JS / client component เดียวของหน้าคือ CollapsibleBlock
|
|
|
|
|
// (หุบ/กางกลุ่ม Bookmark/Annotation)
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
|
|
|
|
|
const UUID_RE =
|
|
|
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
|
|
2026-09-17 19:48:58 +07:00
|
|
|
/** ชื่อย่อต่อชนิดสำหรับบรรทัดสรุปของแต่ละหน้า (ชื่อเต็มอยู่ที่ annotationTypeLabel) */
|
|
|
|
|
const TYPE_SHORT: Record<AnnotationType, string> = {
|
|
|
|
|
HIGHLIGHT: "ไฮไลต์",
|
|
|
|
|
DRAW: "เส้นวาด",
|
|
|
|
|
COMMENT: "คอมเมนต์",
|
|
|
|
|
SHAPE: "รูปทรง",
|
2026-09-21 12:03:16 +07:00
|
|
|
REDACTION: "ปิดพราง",
|
2026-09-17 19:48:58 +07:00
|
|
|
TEXTBOX: "กล่องข้อความ",
|
|
|
|
|
STAMP: "ตรายาง",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** ลำดับชนิดในบรรทัดสรุป — ตามลำดับ enum ให้ deterministic ไม่ขึ้นกับลำดับสร้างรายการ */
|
|
|
|
|
const TYPE_ORDER = Object.keys(TYPE_SHORT) as AnnotationType[];
|
|
|
|
|
|
|
|
|
|
type PageBucket = {
|
|
|
|
|
anns: AnnotationDTO[];
|
|
|
|
|
bms: BookmarkDTO[];
|
|
|
|
|
typeCounts: Partial<Record<AnnotationType, number>>;
|
|
|
|
|
resolvedCount: number;
|
|
|
|
|
participants: Set<string>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** บรรทัดสรุปของหน้า: "ไฮไลต์ 1 · คอมเมนต์ 1 (จัดการแล้ว 1) · โดย ผู้พิพากษา ก., เลขานุการ ข."
|
|
|
|
|
* (หน้าที่มีแต่ที่คั่นหน้า = "โดย …" อย่างเดียว) */
|
|
|
|
|
function pageSummaryLine(b: PageBucket): string {
|
|
|
|
|
const parts: string[] = [];
|
|
|
|
|
for (const t of TYPE_ORDER) {
|
|
|
|
|
const n = b.typeCounts[t];
|
|
|
|
|
if (!n) continue;
|
|
|
|
|
if (t === "COMMENT" && b.resolvedCount > 0)
|
|
|
|
|
parts.push(`คอมเมนต์ ${n} (จัดการแล้ว ${b.resolvedCount})`);
|
|
|
|
|
else parts.push(`${TYPE_SHORT[t]} ${n}`);
|
|
|
|
|
}
|
|
|
|
|
const who = Array.from(b.participants).sort((x, y) => x.localeCompare(y, "th"));
|
|
|
|
|
const shown = who.slice(0, 3).join(", ");
|
|
|
|
|
const whoLabel = who.length > 3 ? `${shown} และอีก ${who.length - 3} คน` : shown;
|
|
|
|
|
return parts.length > 0 ? `${parts.join(" · ")} · โดย ${whoLabel}` : `โดย ${whoLabel}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** focus ring ร่วมของทุกลิงก์ในหน้า — จอสัมผัสได้ 44px อัตโนมัติจากกฎ
|
|
|
|
|
* `main.summary-page a`/`button` ใน globals.css (ห้ามถอด class summary-page
|
|
|
|
|
* ของ <main>) */
|
|
|
|
|
const RING_CLS =
|
|
|
|
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1";
|
|
|
|
|
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
|
|
2026-09-17 19:48:58 +07:00
|
|
|
// design review 2026-09-17 M5: ชื่อแท็บ browser ระบุคดี — เลขานุการเปิดหลายคดี
|
|
|
|
|
// หลายแท็บพร้อมกัน (เดิมทุกแท็บชื่อ "Judge's Note" เหมือนกันหมดแยกไม่ออก)
|
|
|
|
|
export async function generateMetadata({
|
|
|
|
|
params,
|
|
|
|
|
}: {
|
|
|
|
|
params: { id: string };
|
|
|
|
|
}): Promise<Metadata> {
|
|
|
|
|
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`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
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();
|
|
|
|
|
|
2026-09-17 19:48:58 +07:00
|
|
|
const annRows = await db
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
.select()
|
|
|
|
|
.from(annotations)
|
|
|
|
|
.where(eq(annotations.fileId, id))
|
|
|
|
|
.orderBy(asc(annotations.pageIndex), asc(annotations.createdAt));
|
2026-09-17 19:48:58 +07:00
|
|
|
const dtos = annRows.map(toAnnotationDTO);
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
|
2026-09-17 19:48:58 +07:00
|
|
|
// 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));
|
|
|
|
|
|
|
|
|
|
// จัดกลุ่มตามหน้าแบบ union — หน้าปรากฏเมื่อมี annotation หรือ bookmark ≥ 1 รายการ
|
|
|
|
|
const pageMap = new Map<number, PageBucket>();
|
|
|
|
|
const bucketOf = (pageIndex: number): PageBucket => {
|
|
|
|
|
let b = pageMap.get(pageIndex);
|
|
|
|
|
if (!b) {
|
|
|
|
|
b = { anns: [], bms: [], typeCounts: {}, resolvedCount: 0, participants: new Set() };
|
|
|
|
|
pageMap.set(pageIndex, b);
|
|
|
|
|
}
|
|
|
|
|
return b;
|
|
|
|
|
};
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
for (const a of dtos) {
|
2026-09-17 19:48:58 +07:00
|
|
|
const b = bucketOf(a.pageIndex);
|
|
|
|
|
b.anns.push(a);
|
|
|
|
|
b.typeCounts[a.type] = (b.typeCounts[a.type] ?? 0) + 1;
|
|
|
|
|
if (a.type === "COMMENT" && a.resolved) b.resolvedCount += 1;
|
|
|
|
|
b.participants.add(a.userName);
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
}
|
2026-09-17 19:48:58 +07:00
|
|
|
for (const b of bmDtos) {
|
|
|
|
|
const bucket = bucketOf(b.pageIndex);
|
|
|
|
|
bucket.bms.push(b);
|
|
|
|
|
bucket.participants.add(b.userName || "ไม่ทราบชื่อ");
|
|
|
|
|
}
|
|
|
|
|
const pages = Array.from(pageMap.entries()).sort((x, y) => x[0] - y[0]);
|
|
|
|
|
|
2026-09-18 12:09:43 +07:00
|
|
|
// ภาพรวมระดับไฟล์สำหรับรายการตามหน้า desktop (sticky aside) + มือถือ (<details>)
|
2026-09-17 19:48:58 +07:00
|
|
|
const allParticipants = Array.from(
|
|
|
|
|
new Set([
|
|
|
|
|
...dtos.map((a) => a.userName),
|
|
|
|
|
...bmDtos.map((b) => b.userName || "ไม่ทราบชื่อ"),
|
|
|
|
|
]),
|
|
|
|
|
).sort((x, y) => x.localeCompare(y, "th"));
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
|
|
|
|
|
return (
|
2026-09-17 19:48:58 +07:00
|
|
|
<main className="summary-page mx-auto max-w-3xl px-4 py-6 lg:max-w-6xl lg:px-8">
|
|
|
|
|
{/* หัวกระดาษสำนวน — แถบน้ำเงินสืบต่อจาก tile JN ของแบรนด์ (หน้ารายการ/login):
|
|
|
|
|
ชื่อรายงาน + ชื่อแฟ้ม + สถิติในที่เดียว แล้วปิดท้ายด้วยเส้นคั่นบาง */}
|
|
|
|
|
<header className="rounded-xl bg-primary px-5 py-4 text-white shadow-sm sm:px-6">
|
|
|
|
|
<h1 className="text-lg font-semibold leading-snug">สรุปหมายเหตุ</h1>
|
|
|
|
|
<p className="mt-0.5 break-words text-sm text-white/85">{file.title}</p>
|
|
|
|
|
{pages.length > 0 && (
|
|
|
|
|
<p className="mt-2 text-xs tabular-nums text-white/70">
|
|
|
|
|
Annotation {dtos.length} รายการ · Bookmark {bmDtos.length} รายการ ·{" "}
|
|
|
|
|
{pages.length} หน้า
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{/* แถบ action — ghost button ขาวมีขอบ ภาษาเดียวกับการ์ดของหน้า (คำขอผู้ใช้
|
|
|
|
|
2026-09-17 แทนลิงก์ข้อความเปล่า — เห็นเป็นปุ่มชัดขึ้น; จอสัมผัสได้ 44px
|
|
|
|
|
จากกฎ coarse; ตอนพิมพ์ซ่อนทิ้ง — ปุ่มกลับ/export ไม่มีความหมายบนกระดาษ) */}
|
|
|
|
|
<div className="mt-3 flex flex-wrap items-center justify-between gap-x-2 gap-y-1 print:hidden">
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
<Link
|
|
|
|
|
href={`/annotate/${id}`}
|
2026-09-17 19:48:58 +07:00
|
|
|
className={`inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 ${RING_CLS}`}
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
>
|
2026-09-17 19:48:58 +07:00
|
|
|
<ArrowLeft aria-hidden className="h-3.5 w-3.5" />
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
กลับหน้าอ่าน
|
|
|
|
|
</Link>
|
2026-09-17 19:48:58 +07:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{/* F16/D3: export สรุปเป็น CSV (UTF-8 BOM) */}
|
|
|
|
|
<a
|
|
|
|
|
href={withBasePath(`/api/annotations/export-csv?fileId=${id}`)}
|
|
|
|
|
className={`flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
<FileDown aria-hidden className="h-3.5 w-3.5" />
|
|
|
|
|
ส่งออกสรุป (CSV)
|
|
|
|
|
</a>
|
|
|
|
|
{/* N7: ส่งออก Bookmark ทั้งหมดเป็น JSON (label บอกขอบเขตเอง — คู่กับ
|
|
|
|
|
"ส่งออกสรุป (CSV)" ข้างบน ไม่ต้องพึ่ง title tooltip ที่จอสัมผัสใช้ไม่ได้) */}
|
|
|
|
|
<a
|
|
|
|
|
href={withBasePath(`/api/bookmarks/export?fileId=${id}`)}
|
|
|
|
|
className={`flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
<Braces aria-hidden className="h-3.5 w-3.5" />
|
|
|
|
|
ส่งออก Bookmark (JSON)
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{pages.length === 0 ? (
|
2026-09-17 19:48:58 +07:00
|
|
|
<div className="mt-5 rounded-xl border border-dashed border-slate-300 bg-white/70 p-10 text-center">
|
|
|
|
|
<ClipboardList aria-hidden className="mx-auto h-8 w-8 text-slate-300" />
|
|
|
|
|
<p className="mt-3 text-sm font-medium text-slate-600">
|
|
|
|
|
ยังไม่มี Annotation หรือ Bookmark ในไฟล์นี้
|
|
|
|
|
</p>
|
|
|
|
|
<p className="mt-1 text-xs text-slate-500">
|
|
|
|
|
เปิดหน้าอ่านเพื่อมาร์กข้อความ วาดเส้น ใส่คอมเมนต์ หรือคั่นหน้า
|
|
|
|
|
</p>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/annotate/${id}`}
|
|
|
|
|
className={`mt-4 inline-flex rounded-md bg-primary px-4 py-2 text-sm font-medium text-white hover:opacity-90 ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
เปิดหน้าอ่าน
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
) : (
|
2026-09-17 19:48:58 +07:00
|
|
|
<div className="mt-5 lg:grid lg:grid-cols-[220px_minmax(0,1fr)] lg:items-start lg:gap-8">
|
2026-09-18 12:09:43 +07:00
|
|
|
{/* รายการตามหน้ามือถือ (จอ < lg) — design review 2026-09-17 H2: คืน navigation ที่หาย
|
2026-09-17 19:48:58 +07:00
|
|
|
ไปบนมือถือ (aside ด้านล่างแสดงเฉพาะ lg) ใช้ <details> ได้หุบ/กาง + keyboard
|
|
|
|
|
โดยไม่มี JS เพิ่ม — จุดไข่ปลา + ผู้ร่วมทำ ภาษาเดียวกับ desktop (จอ lg กับ
|
|
|
|
|
ตอนพิมพ์ซ่อน — display:none ไม่กินช่องของกริด) */}
|
|
|
|
|
<details className="group rounded-xl border border-slate-200 bg-white shadow-sm lg:hidden print:hidden">
|
|
|
|
|
<summary className="flex cursor-pointer list-none items-center justify-between rounded-md px-4 py-3 text-sm font-semibold text-slate-800 hover:text-primary [&::-webkit-details-marker]:hidden">
|
2026-09-18 12:09:43 +07:00
|
|
|
รายการตามหน้าและผู้ร่วมทำ
|
2026-09-17 19:48:58 +07:00
|
|
|
<ChevronDown
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open:rotate-180 motion-reduce:transition-none"
|
|
|
|
|
/>
|
|
|
|
|
</summary>
|
|
|
|
|
<nav
|
2026-09-18 12:09:43 +07:00
|
|
|
aria-label="รายการตามหน้า"
|
2026-09-17 19:48:58 +07:00
|
|
|
className="flex flex-col gap-0.5 border-t border-slate-100 px-2 pb-2 pt-2"
|
|
|
|
|
>
|
|
|
|
|
{pages.map(([pi, b]) => (
|
|
|
|
|
<a
|
|
|
|
|
key={pi}
|
|
|
|
|
href={`#page-${pi + 1}`}
|
|
|
|
|
aria-label={`หน้า ${pi + 1}, ${b.anns.length + b.bms.length} รายการ`}
|
|
|
|
|
className={`flex items-end gap-1.5 rounded-md px-2 py-2 text-sm text-slate-600 hover:text-primary ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="leading-5">หน้า {pi + 1}</span>
|
|
|
|
|
<span
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="mb-1.5 min-w-4 flex-1 border-b border-dotted border-slate-400"
|
|
|
|
|
/>
|
|
|
|
|
<span className="leading-5 tabular-nums text-slate-500">
|
|
|
|
|
{b.anns.length + b.bms.length}
|
|
|
|
|
</span>
|
|
|
|
|
</a>
|
|
|
|
|
))}
|
|
|
|
|
</nav>
|
|
|
|
|
<p className="border-t border-slate-100 px-4 py-3 text-xs leading-relaxed text-slate-600">
|
|
|
|
|
ผู้ร่วมทำ: {allParticipants.join(" · ")}
|
|
|
|
|
</p>
|
|
|
|
|
</details>
|
|
|
|
|
|
2026-09-18 12:09:43 +07:00
|
|
|
{/* รายการตามหน้า (จอ ≥ lg) — จุดไข่ปลาแบบสารบัญของเอกสารราชการ anchor ล้วนไม่มี JS
|
2026-09-17 19:48:58 +07:00
|
|
|
เป็นการ์ดชุดเดียวกับ <details> มือถือและการ์ดรายหน้า (แสดงเฉพาะหน้าที่มี
|
|
|
|
|
รายการ ≥ 1 ตามหลัก minimize choices; max-h กันเอกสารหลายสิบหน้าสูงเกินจอ
|
|
|
|
|
— ครึ่งล่างของ sticky กดไม่ได้ตลอดกาล) */}
|
|
|
|
|
<aside className="hidden rounded-xl border border-slate-200 bg-white p-4 shadow-sm lg:sticky lg:top-6 lg:block lg:max-h-[calc(100vh-3rem)] lg:overflow-y-auto">
|
2026-09-18 12:09:43 +07:00
|
|
|
<h2 className="text-sm font-semibold text-slate-800">รายการตามหน้า</h2>
|
|
|
|
|
<nav aria-label="รายการตามหน้า" className="mt-2 flex flex-col gap-1">
|
2026-09-17 19:48:58 +07:00
|
|
|
{pages.map(([pi, b]) => (
|
|
|
|
|
<a
|
|
|
|
|
key={pi}
|
|
|
|
|
href={`#page-${pi + 1}`}
|
|
|
|
|
aria-label={`หน้า ${pi + 1}, ${b.anns.length + b.bms.length} รายการ`}
|
|
|
|
|
className={`flex items-end gap-1.5 rounded-md px-1 text-sm text-slate-600 hover:text-primary ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="leading-5">หน้า {pi + 1}</span>
|
|
|
|
|
<span
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="mb-1.5 min-w-4 flex-1 border-b border-dotted border-slate-400"
|
|
|
|
|
/>
|
|
|
|
|
<span className="leading-5 tabular-nums text-slate-500">
|
|
|
|
|
{b.anns.length + b.bms.length}
|
|
|
|
|
</span>
|
|
|
|
|
</a>
|
|
|
|
|
))}
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<h2 className="mt-6 border-t border-slate-300/60 pt-4 text-sm font-semibold text-slate-800">
|
|
|
|
|
ผู้ร่วมทำ
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
</h2>
|
2026-09-17 19:48:58 +07:00
|
|
|
<p className="mt-1.5 px-1 text-xs leading-relaxed text-slate-600">
|
|
|
|
|
{allParticipants.join(" · ")}
|
|
|
|
|
</p>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
{/* การ์ดรายหน้า — 1 หน้า 1 การ์ด เรียงลงมาแยกด้วยช่องว่าง (คำขอผู้ใช้
|
|
|
|
|
2026-09-17 แทนแผ่นกระดาษแผ่นเดียวคั่นเส้น); แต่ละการ์ดมีขอบ folio:
|
|
|
|
|
เลขหน้าใหญ่ + เส้นดิ่ง แบบเลขหน้าบนขอบกระดาษ (mt-5 lg:mt-0 — ระยะจาก
|
2026-09-18 12:09:43 +07:00
|
|
|
รายการตามหน้า <details> บนมือถือ; จอ lg กริดจัดให้) */}
|
2026-09-17 19:48:58 +07:00
|
|
|
<div className="mt-5 flex flex-col gap-4 lg:mt-0">
|
|
|
|
|
{pages.map(([pi, b]) => {
|
|
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
key={pi}
|
|
|
|
|
id={`page-${pi + 1}`}
|
|
|
|
|
aria-labelledby={`page-${pi + 1}-title`}
|
|
|
|
|
className="scroll-mt-6 grid grid-cols-[2.75rem_1fr] rounded-xl border border-slate-200 bg-white shadow-sm sm:grid-cols-[3.75rem_1fr]"
|
|
|
|
|
>
|
|
|
|
|
{/* ขอบ folio — "หน้า" เล็ก + เลขใหญ่ชิดเส้นดิ่ง */}
|
|
|
|
|
<div className="border-r border-slate-100 py-4 pl-3 pr-2 text-right sm:pl-4">
|
|
|
|
|
<h2 id={`page-${pi + 1}-title`} className="tabular-nums">
|
|
|
|
|
<span className="block text-[11px] leading-none text-slate-500">
|
|
|
|
|
หน้า
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mt-1 block text-2xl font-semibold leading-none text-primary">
|
|
|
|
|
{pi + 1}
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
</span>
|
2026-09-17 19:48:58 +07:00
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* เนื้อหาของหน้า — บรรทัดสรุปหน้า (ไม่มีหัวข้อคำอธิบาย) /
|
|
|
|
|
Bookmark / Annotation (หุบ-กางได้, รายการเยื้องจากหัวข้อ) */}
|
|
|
|
|
<div className="min-w-0 py-3 pl-3 pr-3 sm:pl-4 sm:pr-5 sm:py-4">
|
|
|
|
|
<div className="flex items-start justify-between gap-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-sm leading-relaxed text-slate-600">
|
|
|
|
|
{pageSummaryLine(b)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/annotate/${id}?page=${pi + 1}`}
|
|
|
|
|
className={`inline-flex shrink-0 items-center gap-0.5 text-xs text-primary hover:underline ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
เปิดหน้านี้
|
|
|
|
|
<ChevronRight aria-hidden className="h-3 w-3" />
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ที่คั่นหน้าของหน้านี้ทั้งหมด (root + sub) เรียงตาม yOffset */}
|
|
|
|
|
{b.bms.length > 0 && (
|
|
|
|
|
<div className="mt-2">
|
|
|
|
|
<CollapsibleBlock kind="bookmark" count={b.bms.length}>
|
|
|
|
|
<ul className="divide-y divide-slate-100">
|
|
|
|
|
{b.bms.map((bm) => {
|
|
|
|
|
const parent = bm.parentId
|
|
|
|
|
? bookmarkById.get(bm.parentId)
|
|
|
|
|
: undefined;
|
|
|
|
|
return (
|
|
|
|
|
<li key={bm.id} className="ml-4">
|
|
|
|
|
<Link
|
|
|
|
|
href={`/annotate/${id}?page=${pi + 1}`}
|
|
|
|
|
className={`group -mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-slate-50 ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
<Bookmark
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="mt-1 h-4 w-4 shrink-0 text-amber-500"
|
|
|
|
|
/>
|
|
|
|
|
<span className="min-w-0 flex-1">
|
|
|
|
|
<span className="block whitespace-pre-wrap break-words text-sm font-medium leading-snug text-slate-800">
|
|
|
|
|
{bm.title}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mt-0.5 block truncate text-xs text-slate-500">
|
|
|
|
|
โดย {bm.userName || "ไม่ทราบชื่อ"}
|
|
|
|
|
{parent ? ` · ใน: ${parent.title}` : ""}
|
|
|
|
|
</span>
|
|
|
|
|
{bm.note && (
|
|
|
|
|
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-slate-500">
|
|
|
|
|
<StickyNote
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="mt-0.5 h-3 w-3 shrink-0 text-amber-400"
|
|
|
|
|
/>
|
|
|
|
|
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
|
|
|
|
{bm.note}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<ChevronRight
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="mt-1.5 h-4 w-4 shrink-0 text-slate-300 group-hover:text-slate-400"
|
|
|
|
|
/>
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
</CollapsibleBlock>
|
|
|
|
|
</div>
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
)}
|
2026-09-17 19:48:58 +07:00
|
|
|
|
|
|
|
|
{/* หมายเหตุของหน้านี้ — แถวคลิกได้ทั้งแถว (เป้าใหญ่บนจอสัมผัส) */}
|
|
|
|
|
{b.anns.length > 0 && (
|
|
|
|
|
<div className={b.bms.length > 0 ? "mt-3" : "mt-2"}>
|
|
|
|
|
<CollapsibleBlock kind="annotation" count={b.anns.length}>
|
|
|
|
|
<ul className="divide-y divide-slate-100">
|
|
|
|
|
{b.anns.map((a) => (
|
|
|
|
|
<li key={a.id} className="ml-4">
|
|
|
|
|
<Link
|
|
|
|
|
href={`/annotate/${id}?ann=${a.id}`}
|
|
|
|
|
className={`group -mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-slate-50 ${RING_CLS}`}
|
|
|
|
|
>
|
|
|
|
|
<AnnotationTypeIcon
|
|
|
|
|
type={a.type}
|
|
|
|
|
data={a.data}
|
|
|
|
|
className="mt-1 h-4 w-4 shrink-0 text-slate-500"
|
|
|
|
|
/>
|
|
|
|
|
<span className="min-w-0 flex-1">
|
|
|
|
|
<span className="block whitespace-pre-wrap break-words text-sm leading-snug text-slate-800">
|
|
|
|
|
{annotationPreviewText(a)}
|
|
|
|
|
{a.type === "COMMENT" && a.resolved && (
|
|
|
|
|
<span className="ml-1.5 whitespace-nowrap rounded border border-emerald-600/50 bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-700">
|
|
|
|
|
จัดการแล้ว
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mt-0.5 block text-xs text-slate-500">
|
|
|
|
|
{annotationTypeLabel(a)} · โดย {a.userName}
|
|
|
|
|
</span>
|
|
|
|
|
{annotationNote(a) && (
|
|
|
|
|
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-slate-500">
|
|
|
|
|
<StickyNote
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="mt-0.5 h-3 w-3 shrink-0 text-amber-400"
|
|
|
|
|
/>
|
|
|
|
|
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
|
|
|
|
{annotationNote(a)}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<ChevronRight
|
|
|
|
|
aria-hidden
|
|
|
|
|
className="mt-1.5 h-4 w-4 shrink-0 text-slate-300 group-hover:text-slate-400"
|
|
|
|
|
/>
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</CollapsibleBlock>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
);
|
|
|
|
|
}
|