eCourt-note/app/annotate/[id]/summary/page.tsx
Your Name fa880b314e
All checks were successful
Deploy eCourt Note Web to UAT Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to UAT Server / Build Web Docker Image (push) Successful in 48s
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to UAT Server / Deploy eCourt Note Web to UAT Server (push) Successful in 2s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 48s
Deploy eCourt Note Web to UAT Server / Notify Deployment Status (push) Successful in 1s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 13s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
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

119 lines
5.1 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";
import { withBasePath } from "@/utils/base-path";
// หน้าสรุปหมายเหตุ (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="summary-page 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={withBasePath(`/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-[11px] 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 leading-relaxed">
{annotationPreviewText(a)}
</span>
{annotationNote(a) && (
<span className="mt-0.5 block whitespace-pre-wrap break-words text-xs leading-relaxed 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>
);
}