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 AppNav from "@/components/AppNav"; import EcourtLinkForm from "@/components/EcourtLinkForm"; 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"; } } /** การ์ดไฟล์ของหน้ารายการ (Genesis kit-card: surface 12px radius ขอบ line ไม่มีเงา * static — hover ยก -2px + shadow-card-hover; tile ไอคอนเป็นกลาง น้ำเงินสงวนให้ของ * interactive ตาม Do&Don't ของ design system) ใช้ร่วมทุก section (N9) */ function FileCard({ file }: { file: FileRow }) { return (
  • {imageFormatFromUrl(file.pdfUrl) ? ( ) : ( )}
    {kindBadge(file.pdfUrl)}
    {file.title} {/* วันที่เป็นข้อมูลที่ผู้ใช้อ่านจริง — ink-soft (ผ่าน 4.5:1); ink-faint เก็บไว้ ให้ placeholder/disabled เท่านั้น (UX review M2 2026-09-23) */} {formatThaiDate(file.createdAt, true)} {/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป็น secondary (หนึ่ง primary ต่อ section ตาม Genesis; การ์ดทั้งกริดคือ passive surface) */}
    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 ( <> {/* listing-page: hook ของกฎ coarse 44px ใน globals.css (อย่าถอด — เหมือน summary-page ของหน้าสรุป) */}
    {/* ชื่อระบบ Latin ล้วน → General Sans tracking แน่นตาม Genesis display type */}

    Judge's Note

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

    {registerError && (
    เปิดเอกสารจากลิงก์ไม่สำเร็จ: {registerError}
    )} {/* การ์ดจำลองลิงก์ e-Court (N8) อยู่เหนือรายการเอกสาร — เป็นทางเข้าเปิดเอกสาร จากระบบ e-Court ไม่ใช่การเพิ่มไฟล์เข้ารายการ (คุมความกว้างเดียวกับ ฟอร์มอัปโหลด) */}

    เปิดเอกสารจากลิงก์ e-Court

    เอกสาร

    {rows.length > 0 && ( {rows.length} ไฟล์ )}
    {/* อัปโหลดไฟล์ (N4) อยู่กับรายการเอกสาร — ไฟล์ที่อัปโหลดเข้ารายการนี้เอง (คุมความกว้างไม่ให้ฟอร์มกินเต็มความกว้างของกริดรายการ) */}
    {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) => ( ))}
    )} )}
    ); }