978 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			978 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| // frontend/app//(protected)/dashboard/page.jsx
 | ||
| "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";
 | ||
| 
 | ||
| const sea = {
 | ||
|   light: "#E6F7FB",
 | ||
|   light2: "#F3FBFD",
 | ||
|   mid: "#2A7F98",
 | ||
|   dark: "#0D5C75",
 | ||
|   textDark: "#0E2932",
 | ||
| };
 | ||
| const can = (user, perm) => new Set(user?.permissions || []).has(perm);
 | ||
| const Tag = ({ children }) => (
 | ||
|   <Badge
 | ||
|     className="rounded-full px-3 py-1 text-xs border-0"
 | ||
|     style={{ background: sea.light, color: sea.dark }}
 | ||
|   >
 | ||
|     {children}
 | ||
|   </Badge>
 | ||
| );
 | ||
| const SidebarItem = ({ label, icon: Icon, active = false, badge }) => (
 | ||
|   <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>
 | ||
| );
 | ||
| const KPI = ({ label, value, icon: Icon, onClick }) => (
 | ||
|   <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() {
 | ||
|   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%)`,
 | ||
|         }}
 | ||
|       >
 | ||
|         <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 — ส่วนที่ 1–4
 | ||
|               </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">
 | ||
|           {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>
 | ||
|           )}
 | ||
| 
 | ||
|           <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 • ส่วนที่ 1–4</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">
 | ||
|                               ส่วนที่ 3–4
 | ||
|                             </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>
 | ||
| 
 | ||
|         <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>
 | ||
|   );
 | ||
| }
 | 
