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>
118 lines
5 KiB
TypeScript
118 lines
5 KiB
TypeScript
import Link from "next/link";
|
|
import { asc, eq } from "drizzle-orm";
|
|
import { notFound } from "next/navigation";
|
|
import { ArrowLeft, MapPin } from "lucide-react";
|
|
|
|
import { db } from "@/db";
|
|
import { annotations, files, toAnnotationDTO, type AnnotationDTO } from "@/db/schema";
|
|
import { annotationNote, annotationPreviewText, annotationTypeLabel } from "@/utils/annotation-display";
|
|
|
|
// หน้าสรุปหมายเหตุ (F15/D2) — server component รวมทุก annotation (ทุกผู้ใช้)
|
|
// จัดกลุ่มตามหน้า พร้อมปุ่มกระโดดเป็น deep link `/annotate/<id>?ann=<uuid>` (F12)
|
|
|
|
const UUID_RE =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
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 rows = await db
|
|
.select()
|
|
.from(annotations)
|
|
.where(eq(annotations.fileId, id))
|
|
.orderBy(asc(annotations.pageIndex), asc(annotations.createdAt));
|
|
const dtos = rows.map(toAnnotationDTO);
|
|
|
|
// จัดกลุ่มตามหน้า (เรียงหน้ามากไปน้อยไม่เกี่ยว — เดินตามลำดับหน้าเสมอ)
|
|
const byPage = new Map<number, AnnotationDTO[]>();
|
|
for (const a of dtos) {
|
|
const list = byPage.get(a.pageIndex);
|
|
if (list) list.push(a);
|
|
else byPage.set(a.pageIndex, [a]);
|
|
}
|
|
const pages = Array.from(byPage.entries()).sort((x, y) => x[0] - y[0]);
|
|
|
|
return (
|
|
<main className="mx-auto max-w-3xl p-6">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<h1 className="min-w-0 flex-1 truncate text-lg font-bold text-primary">
|
|
สรุปหมายเหตุ — {file.title}
|
|
</h1>
|
|
<Link
|
|
href={`/annotate/${id}`}
|
|
className="flex items-center gap-1 rounded-lg border border-slate-300 px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
|
>
|
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
กลับหน้าอ่าน
|
|
</Link>
|
|
{/* F16/D3: export สรุปเป็น CSV (UTF-8 BOM) */}
|
|
<a
|
|
href={`/api/annotations/export-csv?fileId=${id}`}
|
|
className="rounded-lg border border-slate-300 px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
|
>
|
|
Export CSV
|
|
</a>
|
|
</div>
|
|
|
|
{pages.length === 0 ? (
|
|
<p className="mt-10 rounded border border-dashed p-6 text-center text-sm text-slate-500">
|
|
ยังไม่มี annotation ในไฟล์นี้
|
|
</p>
|
|
) : (
|
|
pages.map(([pageIndex, items]) => (
|
|
<section key={pageIndex} className="mt-6">
|
|
<h2 className="border-b border-slate-200 pb-1 text-sm font-semibold">
|
|
หน้า {pageIndex + 1}
|
|
<span className="ml-2 text-xs font-normal text-slate-400">
|
|
{items.length} รายการ
|
|
</span>
|
|
</h2>
|
|
<ul className="mt-2 divide-y divide-slate-100">
|
|
{items.map((a) => (
|
|
<li key={a.id} className="flex items-start gap-3 py-2">
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block text-xs">
|
|
<span className="font-medium">{annotationTypeLabel(a)}</span>
|
|
{a.type === "COMMENT" && a.resolved && (
|
|
<span className="ml-2 rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] text-emerald-600">
|
|
จัดการแล้ว
|
|
</span>
|
|
)}
|
|
<span className="ml-2 text-slate-400">{a.userName}</span>
|
|
</span>
|
|
<span className="mt-0.5 block whitespace-pre-wrap break-words text-sm">
|
|
{annotationPreviewText(a)}
|
|
</span>
|
|
{annotationNote(a) && (
|
|
<span className="mt-0.5 block whitespace-pre-wrap break-words text-xs text-slate-500">
|
|
โน้ต: {annotationNote(a)}
|
|
</span>
|
|
)}
|
|
</span>
|
|
{/* ปุ่มกระโดด = deep link ไปยัง annotation นั้น (F12 รับ ?ann= ตอน mount) */}
|
|
<Link
|
|
href={`/annotate/${id}?ann=${a.id}`}
|
|
className="flex shrink-0 items-center gap-1 rounded-md border border-slate-300 px-2 py-1 text-[11px] text-slate-600 hover:bg-slate-100"
|
|
>
|
|
<MapPin className="h-3 w-3" />
|
|
ไปตำแหน่ง
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
))
|
|
)}
|
|
</main>
|
|
);
|
|
}
|