eCourt-note/app/page.tsx

215 lines
9.1 KiB
TypeScript

import Link from "next/link";
import { redirect } from "next/navigation";
import { asc } from "drizzle-orm";
import { FileImage, FileText, Folder } from "lucide-react";
import { db } from "@/db";
import { files } from "@/db/schema";
import EcourtLinkForm from "@/components/EcourtLinkForm";
import SessionBar from "@/components/SessionBar";
import UploadFileForm from "@/components/UploadFileForm";
import { imageFormatFromUrl } from "@/utils/file-kind";
import { formatThaiDate } from "@/utils/date-format";
import { getSessionUser } from "@/lib/session";
import { registerExternalFile } from "@/lib/external-import";
import { firstSearchParam } from "@/utils/external-file";
export const dynamic = "force-dynamic";
type FileRow = typeof files.$inferSelect;
/** badge ชนิดไฟล์ต่อรายการ (N3) */
function kindBadge(pdfUrl: string): string {
switch (imageFormatFromUrl(pdfUrl)) {
case "jpeg":
return "JPG";
case "png":
return "PNG";
case "tiff":
return "TIFF";
default:
return "PDF";
}
}
/** การ์ดไฟล์ของหน้ารายการ — ใช้ร่วมทุก section (N9 จัดกลุ่มแล้วยังหน้าตาเดิม) */
function FileCard({ file }: { file: FileRow }) {
return (
<li className="group flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-primary/30 hover:shadow-md">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
{imageFormatFromUrl(file.pdfUrl) ? (
<FileImage className="h-5 w-5 text-primary" />
) : (
<FileText className="h-5 w-5 text-primary" />
)}
</div>
<span className="min-w-0 flex-1">
<span className="block break-all line-clamp-2 text-sm font-medium text-slate-900">
{file.title}
</span>
<span className="mt-0.5 block text-xs text-slate-500">
<span className="mr-1.5 rounded bg-slate-100 px-1 py-0.5 font-medium text-slate-500">
{kindBadge(file.pdfUrl)}
</span>
{formatThaiDate(file.createdAt, true)}
</span>
</span>
{/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป้าจิ้ม 36px (H4) */}
<Link
href={`/annotate/${file.id}`}
className="shrink-0 rounded-md bg-primary px-4 py-2 text-sm font-medium text-white transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
>
Annotate
</Link>
</li>
);
}
export default async function HomePage({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
// defense ซ้อน middleware — หน้านี้อ่าน cookie อยู่ดีจึงกันซ้ำไม่ให้ render รายการให้คนไม่มี session
const user = getSessionUser();
if (!user) redirect("/login");
// N8: เข้าจากลิงก์ e-Court ?fileUrl=&fileId=&typeId= — ลงทะเบียนเอกสาร (metadata-only
// dedupe ด้วย external_id) แล้วพาเข้า viewer เลย — redirect() โยน NEXT_REDIRECT
// ต้องเรียกนอก try/catch เสมอ; หน้านี้ force-dynamic จึงไม่ต้อง revalidatePath
// (entry ที่สองอยู่ที่ /annotate?fileUrl=… — app/annotate/page.tsx)
let registerError: string | null = null;
const fileUrl = firstSearchParam(searchParams.fileUrl);
if (fileUrl) {
const result = await registerExternalFile(
fileUrl,
firstSearchParam(searchParams.fileId),
firstSearchParam(searchParams.typeId),
);
if (result.ok) redirect(`/annotate/${result.fileId}`);
registerError = result.error;
}
const rows = await db.select().from(files).orderBy(asc(files.createdAt));
// N9: จัดกลุ่มตาม type_id — การ์ดในกลุ่มเรียง createdAt เหมือนเดิม (ตาม query ที่ asc)
// ส่วนลำดับกลุ่มเรียงตาม createdAt ของไฟล์ล่าสุดในกลุ่ม (กลุ่มที่มี activity ใหม่อยู่บน)
// ไฟล์ type_id NULL (seed/อัปโหลด N4/ลิงก์ไม่มี typeId) = section "ไม่มีกลุ่ม" ท้ายสุด
const grouped = new Map<string, FileRow[]>();
const ungrouped: FileRow[] = [];
for (const f of rows) {
if (f.typeId) {
const list = grouped.get(f.typeId);
if (list) list.push(f);
else grouped.set(f.typeId, [f]);
} else {
ungrouped.push(f);
}
}
const sections = Array.from(grouped.entries()).sort(
(a, b) =>
b[1][b[1].length - 1].createdAt.getTime() -
a[1][a[1].length - 1].createdAt.getTime(),
);
return (
<main className="mx-auto max-w-2xl px-4 py-10 sm:px-8">
<header className="flex items-center gap-3">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-primary text-base font-bold text-white shadow-sm">
JN
</div>
<div>
<h1 className="text-2xl font-bold leading-tight text-primary">
Judge&apos;s Note
</h1>
<p className="text-sm text-slate-500">
ระบบ PDF Annotation สำหรับ e-Court
</p>
</div>
</header>
<SessionBar userName={user.userName} />
{registerError && (
<div
role="alert"
className="mt-6 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700"
>
เปิดเอกสารจากลิงก์ไม่สำเร็จ: {registerError}
</div>
)}
{/* การ์ดเปิดเอกสารจากลิงก์ e-Court — บล็อกแยกของตัวเอง มีหัวเรื่องในตัวแล้ว (N8) */}
<EcourtLinkForm />
<section className="mt-8">
<h2 className="text-sm font-semibold text-slate-700">เพิ่มเอกสาร</h2>
<UploadFileForm />
</section>
<div className="mt-8 flex items-baseline justify-between">
<h2 className="text-sm font-semibold text-slate-700">เอกสาร</h2>
{rows.length > 0 && (
<span className="text-xs tabular-nums text-slate-500">{rows.length} ไฟล์</span>
)}
</div>
{rows.length === 0 ? (
<div className="mt-3 rounded-xl border border-dashed border-slate-300 bg-white/60 p-10 text-center">
<FileText className="mx-auto h-8 w-8 text-slate-300" />
<p className="mt-3 text-sm font-medium text-slate-500">
ยังไม่มีไฟล์ในระบบ
</p>
<p className="mt-1 text-xs text-slate-500">
รัน /sync-db เพื่อ seed ไฟล์ตัวอย่าง
</p>
</div>
) : (
<>
{sections.map(([typeId, list]) => (
<section key={typeId} className="mt-6">
<div className="flex items-center gap-2 border-b border-slate-200 pb-2">
<Folder className="h-4 w-4 shrink-0 text-primary" />
<span className="min-w-0 break-all text-sm font-semibold text-slate-700">
{typeId}
</span>
<span className="shrink-0 text-xs tabular-nums text-slate-500">
{list.length} ไฟล์
</span>
{/* Next Link เติม basePath ให้เอง (invariant #12) */}
<Link
href={`/groups/${encodeURIComponent(typeId)}`}
className="ml-auto shrink-0 rounded text-xs font-medium text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
>
เอกสารในกลุ่ม
</Link>
</div>
<ul className="mt-3 space-y-3">
{list.map((f) => (
<FileCard key={f.id} file={f} />
))}
</ul>
</section>
))}
{ungrouped.length > 0 && (
<section className="mt-6">
<div className="flex items-center gap-2 border-b border-slate-200 pb-2">
<span className="text-sm font-semibold text-slate-700">
ไม่มีกลุ่ม
</span>
<span className="text-xs tabular-nums text-slate-500">
{ungrouped.length} ไฟล์
</span>
</div>
<ul className="mt-3 space-y-3">
{ungrouped.map((f) => (
<FileCard key={f.id} file={f} />
))}
</ul>
</section>
)}
</>
)}
</main>
);
}