400 lines
27 KiB
JavaScript
Executable File
400 lines
27 KiB
JavaScript
Executable File
"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 — ส่วนที่ 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">
|
||
{/* 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 • ส่วนที่ 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>
|
||
|
||
{/* 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)
|
||
|
||
คงธีมโทนน้ำทะเลเข้ม/อ่อนตามที่กำหน |