Files

400 lines
27 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React from "react";
import Link from "next/link";
import { motion } from "framer-motion";
import {
LayoutDashboard, FileText, Files, Send, Layers, Users, Settings, Activity,
Search, ChevronRight, ShieldCheck, Workflow, Database, Mail, Server, Shield, BookOpen,
PanelLeft, PanelRight, ChevronDown, Plus, Filter, Eye, EyeOff, SlidersHorizontal, Columns3, X, ExternalLink
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Progress } from "@/components/ui/progress";
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel } from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { API_BASE } from "@/lib/api";
// Sea palette
const sea = { light: "#E6F7FB", light2: "#F3FBFD", mid: "#2A7F98", dark: "#0D5C75", textDark: "#0E2932" };
// RBAC helper (client-side; backend truth comes from /auth/me)
function can(user, perm){
const set = new Set(user?.permissions || []);
return set.has(perm);
}
function Tag({ children }) {
return (
<Badge className="rounded-full px-3 py-1 text-xs border-0" style={{ background: sea.light, color: sea.dark }}>{children}</Badge>
);
}
function SidebarItem({ label, icon: Icon, active=false, badge }) {
return (
<button className={`group w-full flex items-center gap-3 rounded-2xl px-4 py-3 text-left transition-all border ${active?"bg-white/70":"bg-white/30 hover:bg-white/60"}`} style={{ borderColor: "#ffffff40", color: sea.textDark }}>
<Icon className="h-5 w-5" />
<span className="grow font-medium">{label}</span>
{badge ? <span className="text-xs rounded-full px-2 py-0.5" style={{ background: sea.light, color: sea.dark }}>{badge}</span> : null}
<ChevronRight className="h-4 w-4 opacity-0 group-hover:opacity-100 transition-opacity" />
</button>
);
}
function KPI({ label, value, icon: Icon, onClick }) {
return (
<Card onClick={onClick} className="rounded-2xl shadow-sm border-0 cursor-pointer hover:shadow transition" style={{ background: "white" }}>
<CardContent className="p-5">
<div className="flex items-start justify-between">
<span className="text-sm opacity-70">{label}</span>
<div className="rounded-xl p-2" style={{ background: sea.light }}>
<Icon className="h-5 w-5" style={{ color: sea.dark }} />
</div>
</div>
<div className="mt-3 text-3xl font-bold" style={{ color: sea.textDark }}>{value}</div>
<div className="mt-2"><Progress value={Math.min(100, (value/400)*100)} /></div>
</CardContent>
</Card>
);
}
function PreviewDrawer({ open, onClose, children }) {
return (
<div className={`fixed top-0 right-0 h-full w-full sm:w-[420px] bg-white shadow-2xl transition-transform z-50 ${open ? "translate-x-0" : "translate-x-full"}`}>
<div className="flex items-center justify-between p-4 border-b">
<div className="font-medium">รายละเอยด</div>
<Button variant="ghost" size="icon" onClick={onClose}><X className="h-5 w-5"/></Button>
</div>
<div className="p-4 overflow-auto h-[calc(100%-56px)]">{children}</div>
</div>
);
}
export default function DashboardPage(){
// ดึง user จริงจาก /auth/me (layout ฝั่ง SSR ตรวจสิทธิ์แล้ว)
const [user, setUser] = React.useState(null);
const [sidebarOpen, setSidebarOpen] = React.useState(true);
const [densityCompact, setDensityCompact] = React.useState(false);
const [showCols, setShowCols] = React.useState({ type:true, id:true, title:true, status:true, due:true, owner:true, actions:true });
const [previewOpen, setPreviewOpen] = React.useState(false);
const [filters, setFilters] = React.useState({ type:"All", status:"All", overdue:false });
const [activeQuery, setActiveQuery] = React.useState({});
React.useEffect(()=>{
fetch(`${API_BASE}/auth/me`, { credentials: "include" })
.then(r=> r.ok ? r.json() : null)
.then(data => setUser(data?.user || null))
.catch(()=> setUser(null));
},[]);
const quickLinks = [
{ label: "สร้าง RFA", icon: FileText, perm: "rfa:create", href: "/rfas/new" },
{ label: "อัปโหลด Drawing", icon: Layers, perm: "drawing:upload", href: "/drawings/upload" },
{ label: "สร้าง Transmittal", icon: Send, perm: "transmittal:create", href: "/transmittals/new" },
{ label: "บันทึกหนังสือสื่อสาร", icon: Mail, perm: "correspondence:create", href: "/correspondences/new" },
];
const nav = [
{ label: "แดชบอร์ด", icon: LayoutDashboard },
{ label: "Drawings", icon: Layers },
{ label: "RFAs", icon: FileText },
{ label: "Transmittals", icon: Send },
{ label: "Contracts & Volumes", icon: BookOpen },
{ label: "Correspondences", icon: Files },
{ label: "ผู้ใช้/บทบาท", icon: Users, perm: "users:manage" },
{ label: "Reports", icon: Activity },
{ label: "Workflow (n8n)", icon: Workflow, perm: "workflow:view" },
{ label: "Health", icon: Server, perm: "health:view" },
{ label: "Admin", icon: Settings, perm: "admin:view" },
];
const kpis = [
{ key: "rfa-pending", label: "RFAs รออนุมัติ", value: 12, icon: FileText, query: { type: "RFA", status: "pending" } },
{ key: "drawings", label: "แบบ (Drawings) ล่าสุด", value: 326, icon: Layers, query: { type: "Drawing" } },
{ key: "trans-month", label: "Transmittals เดือนนี้", value: 18, icon: Send, query: { type: "Transmittal", month: "current" } },
{ key: "overdue", label: "เกินกำหนด (Overdue)", value: 5, icon: Activity, query: { overdue: true } },
];
const recent = [
{ type: "RFA", code: "RFA-LCP3-0012", title: "ปรับปรุงรายละเอียดเสาเข็มท่าเรือ", who: "สุรเชษฐ์ (Editor)", when: "เมื่อวาน 16:40" },
{ type: "Drawing", code: "DWG-C-210A-Rev.3", title: "แปลนโครงสร้างท่าเรือส่วนที่ 2", who: "วรวิชญ์ (Admin)", when: "วันนี้ 09:15" },
{ type: "Transmittal", code: "TR-2025-0916-04", title: "ส่งแบบ Rebar Shop Drawing ชุด A", who: "Supansa (Viewer)", when: "16 ก.ย. 2025" },
{ type: "Correspondence", code: "CRSP-58", title: "แจ้งเลื่อนประชุมตรวจแบบ", who: "Kitti (Editor)", when: "15 ก.ย. 2025" },
];
const items = [
{t:"RFA", id:"RFA-LCP3-0013", title:"ยืนยันรายละเอียดท่อระบายน้ำ", status:"Pending", due:"20 ก.ย. 2025", owner:"คุณแดง"},
{t:"Drawing", id:"DWG-S-115-Rev.1", title:"Section เสาเข็มพื้นที่ส่วนที่ 1", status:"Review", due:"19 ก.ย. 2025", owner:"วิทยา"},
{t:"Transmittal", id:"TR-2025-0915-03", title:"ส่งแบบโครงสร้างท่าเรือ ชุด B", status:"Sent", due:"—", owner:"สุธิดา"},
];
const visibleItems = items.filter(r => {
if (filters.type !== "All" && r.t !== filters.type) return false;
if (filters.status !== "All" && r.status !== filters.status) return false;
if (filters.overdue && r.due === "—") return false;
return true;
});
const onKpiClick = (q) => {
setActiveQuery(q);
if (q?.type) setFilters(f => ({...f, type:q.type}));
if (q?.overdue) setFilters(f => ({...f, overdue:true}));
};
return (
<TooltipProvider>
<div className="min-h-screen" style={{ background: `linear-gradient(180deg, ${sea.light2} 0%, ${sea.light} 100%)` }}>
{/* Top System + Quick Actions */}
<header className="sticky top-0 z-40 backdrop-blur-md border-b" style={{ borderColor: "#ffffff66", background: "rgba(230,247,251,0.7)" }}>
<div className="mx-auto max-w-7xl px-4 py-2 flex items-center gap-3">
<button className="h-9 w-9 rounded-2xl flex items-center justify-center shadow-sm" style={{ background: sea.dark }} onClick={()=>setSidebarOpen(v=>!v)} aria-label={sidebarOpen?"ซ่อนแถบด้านข้าง":"แสดงแถบด้านข้าง"}>
{sidebarOpen ? <PanelLeft className="h-5 w-5 text-white"/> : <PanelRight className="h-5 w-5 text-white"/>}
</button>
<div>
<div className="text-xs opacity-70">Document Management System</div>
<div className="font-semibold" style={{ color: sea.textDark }}>โครงการพฒนาทาเรอแหลมฉบ ระยะท 3 วนท 14</div>
</div>
<Tag>Phase 3</Tag><Tag>Port Infrastructure</Tag>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="ml-auto rounded-2xl btn-sea flex items-center gap-2">System <ChevronDown className="h-4 w-4"/></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-56">
<DropdownMenuLabel>ระบบ</DropdownMenuLabel>
{can(user,'admin:view') && <DropdownMenuItem><Settings className="h-4 w-4 mr-2"/> Admin</DropdownMenuItem>}
{can(user,'users:manage') && <DropdownMenuItem><Users className="h-4 w-4 mr-2"/> ใช/บทบาท</DropdownMenuItem>}
{can(user,'health:view') && <DropdownMenuItem asChild><a href="/health" className="flex items-center w-full"><Server className="h-4 w-4 mr-2"/> Health <ExternalLink className="h-3 w-3 ml-auto"/></a></DropdownMenuItem>}
{can(user,'workflow:view') && <DropdownMenuItem asChild><a href="/workflow" className="flex items-center w-full"><Workflow className="h-4 w-4 mr-2"/> Workflow (n8n) <ExternalLink className="h-3 w-3 ml-auto"/></a></DropdownMenuItem>}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="rounded-2xl btn-sea ml-2"><Plus className="h-4 w-4 mr-1"/> New</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{quickLinks.map(({label, icon:Icon, perm, href}) => (
can(user,perm) ? (
<DropdownMenuItem key={label} asChild>
<Link href={href} className="flex items-center"><Icon className="h-4 w-4 mr-2"/>{label}</Link>
</DropdownMenuItem>
) : (
<Tooltip key={label}>
<TooltipTrigger asChild>
<div className="px-2 py-1.5 text-sm opacity-40 cursor-not-allowed flex items-center"><Icon className="h-4 w-4 mr-2"/>{label}</div>
</TooltipTrigger>
<TooltipContent>ไมทธใชงาน ({perm})</TooltipContent>
</Tooltip>
)
))}
<DropdownMenuSeparator />
<DropdownMenuItem><Layers className="h-4 w-4 mr-2"/> Import / Bulk upload</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
<div className="mx-auto max-w-7xl px-4 py-6 grid grid-cols-12 gap-6">
{/* Sidebar */}
{sidebarOpen && (
<aside className="col-span-12 lg:col-span-3 xl:col-span-3">
<div className="rounded-3xl p-4 border" style={{ background: "rgba(255,255,255,0.7)", borderColor: "#ffffff66" }}>
<div className="mb-3 flex items-center gap-2">
<ShieldCheck className="h-5 w-5" style={{ color: sea.dark }} />
<div className="text-sm">RBAC: <span className="font-medium">{user?.role || '—'}</span></div>
</div>
<div className="relative mb-3">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 opacity-70" />
<Input placeholder="ค้นหา RFA / Drawing / Transmittal / Code…" className="pl-9 rounded-2xl border-0 bg-white"/>
</div>
<div className="rounded-2xl p-3 border mb-3" style={{ borderColor: "#eef6f8", background: "#ffffffaa" }}>
<div className="text-xs font-medium mb-2">วกรอง</div>
<div className="grid grid-cols-2 gap-2">
<select className="rounded-xl border p-2 text-sm" value={filters.type} onChange={e=>setFilters(f=>({...f,type:e.target.value}))}>
<option>All</option><option>RFA</option><option>Drawing</option><option>Transmittal</option><option>Correspondence</option>
</select>
<select className="rounded-xl border p-2 text-sm" value={filters.status} onChange={e=>setFilters(f=>({...f,status:e.target.value}))}>
<option>All</option><option>Pending</option><option>Review</option><option>Sent</option>
</select>
<label className="col-span-2 flex items-center gap-2 text-sm">
<Switch checked={filters.overdue} onCheckedChange={(v)=>setFilters(f=>({...f, overdue:v}))}/> แสดงเฉพาะ Overdue
</label>
</div>
<div className="mt-2 flex gap-2">
<Button size="sm" variant="outline" className="rounded-xl" style={{ borderColor: sea.mid, color: sea.dark }}><Filter className="h-4 w-4 mr-1"/>Apply</Button>
<Button size="sm" variant="ghost" className="rounded-xl" onClick={()=>setFilters({type:"All",status:"All",overdue:false})}>Reset</Button>
</div>
</div>
<div className="space-y-2">
{nav.filter(item => !item.perm || can(user,item.perm)).map((n,i)=> (
<SidebarItem key={n.label} label={n.label} icon={n.icon} active={i===0} badge={n.label === "RFAs" ? 12 : undefined} />
))}
</div>
<div className="mt-5 text-xs opacity-70 flex items-center gap-2">
<Database className="h-4 w-4"/> dms_db MariaDB 10.11
</div>
</div>
</aside>
)}
{/* Content */}
<main className={`col-span-12 ${sidebarOpen?"lg:col-span-9 xl:col-span-9":""} space-y-6`}>
<motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.05, duration: 0.4 }}>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
{kpis.map((k)=> <KPI key={k.key} {...k} onClick={()=>onKpiClick(k.query)} />)}
</div>
</motion.div>
<div className="flex items-center justify-between">
<div className="text-sm opacity-70">ผลลพธจากตวกรอง: {filters.type}/{filters.status}{filters.overdue?" • Overdue":""}</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" className="rounded-xl" style={{ borderColor: sea.mid, color: sea.dark }} onClick={()=>setDensityCompact(v=>!v)}>
<SlidersHorizontal className="h-4 w-4 mr-1"/> Density: {densityCompact?"Compact":"Comfort"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="outline" className="rounded-xl" style={{ borderColor: sea.mid, color: sea.dark }}>
<Columns3 className="h-4 w-4 mr-1"/> Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.keys(showCols).map(key => (
<DropdownMenuItem key={key} onClick={() => setShowCols(s=>({...s, [key]: !s[key]}))}>
{(showCols[key]?<Eye className="h-4 w-4 mr-2"/>:<EyeOff className="h-4 w-4 mr-2"/>)}{key}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<Card className="rounded-2xl border-0"><CardContent className="p-0">
<div className="overflow-x-auto">
<table className={`min-w-full text-sm ${densityCompact?"[&_*]:py-1":""}`}>
<thead className="sticky top-[56px] z-10" style={{ background:"white", borderBottom:"1px solid #efefef" }}>
<tr className="text-left">
{showCols.type && <th className="py-2 px-3">ประเภท</th>}
{showCols.id && <th className="py-2 px-3">รห</th>}
{showCols.title && <th className="py-2 px-3">อเรอง</th>}
{showCols.status && <th className="py-2 px-3">สถานะ</th>}
{showCols.due && <th className="py-2 px-3">กำหนดส</th>}
{showCols.owner && <th className="py-2 px-3">บผดชอบ</th>}
{showCols.actions && <th className="py-2 px-3">ดการ</th>}
</tr>
</thead>
<tbody>
{visibleItems.length === 0 && (<tr><td className="py-8 px-3 text-center opacity-70" colSpan={7}>ไมพบรายการตามตวกรองทเลอก</td></tr>)}
{visibleItems.map(row => (
<tr key={row.id} className="border-b hover:bg-gray-50/50 cursor-pointer" style={{ borderColor: "#f3f3f3" }} onClick={()=>setPreviewOpen(true)}>
{showCols.type && <td className="py-2 px-3">{row.t}</td>}
{showCols.id && <td className="py-2 px-3 font-mono">{row.id}</td>}
{showCols.title && <td className="py-2 px-3">{row.title}</td>}
{showCols.status && <td className="py-2 px-3"><Tag>{row.status}</Tag></td>}
{showCols.due && <td className="py-2 px-3">{row.due}</td>}
{showCols.owner && <td className="py-2 px-3">{row.owner}</td>}
{showCols.actions && <td className="py-2 px-3"><div className="flex gap-2"><Button size="sm" className="rounded-xl btn-sea">เป</Button><Button size="sm" variant="outline" className="rounded-xl" style={{ borderColor: sea.mid, color: sea.dark }}>Assign</Button></div></td>}
</tr>
))}
</tbody>
</table>
</div>
<div className="px-4 py-2 text-xs opacity-70 border-t" style={{ borderColor: "#efefef"}}>เคลดล: ใช / เลอนแถว, Enter เป, / </div>
</CardContent></Card>
<Tabs defaultValue="overview" className="w-full">
<TabsList className="rounded-2xl border bg-white/80" style={{ borderColor: "#ffffff80" }}>
<TabsTrigger value="overview">ภาพรวม</TabsTrigger>
<TabsTrigger value="reports">รายงาน</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4 space-y-4">
<div className="grid lg:grid-cols-5 gap-4">
<Card className="rounded-2xl border-0 lg:col-span-3"><CardContent className="p-5">
<div className="flex items-center justify-between"><div className="font-semibold" style={{ color: sea.textDark }}>สถานะโครงการ</div><Tag>Phase 3 14</Tag></div>
<div className="mt-4 space-y-3">
<div><div className="text-sm opacity-70">ความคบหนาโดยรวม</div><Progress value={62} /></div>
<div className="grid grid-cols-3 gap-3">
<div className="rounded-xl p-4 border" style={{ background: sea.light, borderColor: sea.light }}><div className="text-xs opacity-70">วนท 1</div><div className="text-lg font-semibold"> 70%</div></div>
<div className="rounded-xl p-4 border" style={{ background: sea.light, borderColor: sea.light }}><div className="text-xs opacity-70">วนท 2</div><div className="text-lg font-semibold"> 58%</div></div>
<div className="rounded-xl p-4 border" style={{ background: sea.light, borderColor: sea.light }}><div className="text-xs opacity-70">วนท 34</div><div className="text-lg font-semibold"> 59%</div></div>
</div>
</div>
</CardContent></Card>
<Card className="rounded-2xl border-0 lg:col-span-2"><CardContent className="p-5 space-y-3">
<div className="flex items-center justify-between"><div className="font-semibold" style={{ color: sea.textDark }}>System Health</div><Tag>QNAP Container Station</Tag></div>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2"><Server className="h-4 w-4"/> Nginx Reverse Proxy <span className="ml-auto font-medium" style={{ color: sea.dark }}>Healthy</span></div>
<div className="flex items-center gap-2"><Database className="h-4 w-4"/> MariaDB 10.11 <span className="ml-auto font-medium" style={{ color: sea.dark }}>OK</span></div>
<div className="flex items-center gap-2"><Workflow className="h-4 w-4"/> n8n (Postgres) <span className="ml-auto font-medium" style={{ color: sea.dark }}>OK</span></div>
<div className="flex items-center gap-2"><Shield className="h-4 w-4"/> RBAC Enforcement <span className="ml-auto font-medium" style={{ color: sea.dark }}>Enabled</span></div>
</div>
<div className="pt-2 border-t" style={{ borderColor: "#eeeeee" }}>
<Button variant="outline" className="rounded-xl" style={{ borderColor: sea.mid, color: sea.dark }}>เปดหน /health</Button>
</div>
</CardContent></Card>
</div>
<Card className="rounded-2xl border-0"><CardContent className="p-5">
<div className="flex items-center justify-between mb-3"><div className="font-semibold" style={{ color: sea.textDark }}>จกรรมลาส</div><div className="flex gap-2"><Tag>Admin</Tag><Tag>Editor</Tag><Tag>Viewer</Tag></div></div>
<div className="grid md:grid-cols-2 xl:grid-cols-4 gap-3">
{recent.map((r)=> (
<div key={r.code} className="rounded-2xl p-4 border hover:shadow-sm transition" style={{ background: "white", borderColor: "#efefef" }}>
<div className="text-xs opacity-70">{r.type} {r.code}</div>
<div className="font-medium mt-1" style={{ color: sea.textDark }}>{r.title}</div>
<div className="text-xs mt-2 opacity-70">{r.who}</div>
<div className="text-xs opacity-70">{r.when}</div>
</div>
))}
</div>
</CardContent></Card>
</TabsContent>
<TabsContent value="reports" className="mt-4">
<div className="grid lg:grid-cols-2 gap-4">
<Card className="rounded-2xl border-0"><CardContent className="p-5"><div className="font-semibold mb-2" style={{ color: sea.textDark }}>Report A: RFA Drawings Revisions</div><div className="text-sm opacity-70"> Drawing Revision + Code</div><div className="mt-3"><Button className="rounded-2xl btn-sea">Export CSV</Button></div></CardContent></Card>
<Card className="rounded-2xl border-0"><CardContent className="p-5"><div className="font-semibold mb-2" style={{ color: sea.textDark }}>Report B: ไทมไลน RFA vs Drawing Rev</div><div className="text-sm opacity-70"> Query #2 </div><div className="mt-3"><Button className="rounded-2xl btn-sea"></Button></div></CardContent></Card>
</div>
</TabsContent>
</Tabs>
<div className="text-xs opacity-70 text-center py-6">Sea-themed Dashboard Sidebar อนได RBAC แสดง/อน Faceted search KPI click-through Preview drawer Column visibility/Density</div>
</main>
</div>
{/* Drawer */}
<PreviewDrawer open={previewOpen} onClose={()=>setPreviewOpen(false)}>
<div className="space-y-2 text-sm">
<div><span className="opacity-70">รห:</span> RFA-LCP3-0013</div>
<div><span className="opacity-70">อเรอง:</span> </div>
<div><span className="opacity-70">สถานะ:</span> <Tag>Pending</Tag></div>
<div><span className="opacity-70">แนบไฟล:</span> 2 (PDF, DWG)</div>
<div className="pt-2 flex gap-2">
{can(user,'rfa:create') && <Button className="btn-sea rounded-xl">แกไข</Button>}
<Button variant="outline" className="rounded-xl" style={{ borderColor: sea.mid, color: sea.dark }}>เปดเตมหน</Button>
</div>
</div>
</PreviewDrawer>
<style jsx global>{`
.btn-sea { background: ${sea.dark}; }
.btn-sea:hover { background: ${sea.mid}; }
.menu-sea { background: ${sea.dark}; }
.menu-sea:hover { background: ${sea.mid}; }
`}</style>
</div>
</TooltipProvider>
);
}
งสำคญในไฟล:
Topbar: System (Admin/Users/Health/Workflow) + New (RFA/Transmittal/Correspondence/Upload Drawing) อน/ดตาม permissions
Sidebar: Search + วกรอง (Type/Status/Overdue) + เมนพรอม badge
Main: KPI การดคลกได sync ลเตอร, ตารางพรอม Density toggle / Column visibility, และ Preview Drawer
ใช API_BASE จาก @/lib/api (คอนเทนเนอรงค NEXT_PUBLIC_API_BASE)
คงธมโทนนำทะเลเข/อนตามทกำหน