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 (
  • {imageFormatFromUrl(file.pdfUrl) ? ( ) : ( )}
    {file.title} {kindBadge(file.pdfUrl)} {formatThaiDate(file.createdAt, true)} {/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป้าจิ้ม 36px (H4) */} Annotate
  • ); } export default async function HomePage({ searchParams, }: { searchParams: Record; }) { // 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(); 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 (
    JN

    Judge's Note

    ระบบ PDF Annotation สำหรับ e-Court

    {registerError && (
    เปิดเอกสารจากลิงก์ไม่สำเร็จ: {registerError}
    )} {/* การ์ดเปิดเอกสารจากลิงก์ e-Court — บล็อกแยกของตัวเอง มีหัวเรื่องในตัวแล้ว (N8) */}

    เพิ่มเอกสาร

    เอกสาร

    {rows.length > 0 && ( {rows.length} ไฟล์ )}
    {rows.length === 0 ? (

    ยังไม่มีไฟล์ในระบบ

    รัน /sync-db เพื่อ seed ไฟล์ตัวอย่าง

    ) : ( <> {sections.map(([typeId, list]) => (
    {typeId} {list.length} ไฟล์ {/* Next Link เติม basePath ให้เอง (invariant #12) */} เอกสารในกลุ่ม
      {list.map((f) => ( ))}
    ))} {ungrouped.length > 0 && (
    ไม่มีกลุ่ม {ungrouped.length} ไฟล์
      {ungrouped.map((f) => ( ))}
    )} )}
    ); }