Files
InaTrading-Portal/src/app/admin/dashboard/page.tsx

251 lines
12 KiB
TypeScript

"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { getBackendErrorMessage } from "@/lib/error-message";
interface AdminQueueItem {
id: string;
name: string;
seller: string;
date: string;
note: string;
market: string;
totalStock: number;
minPrice: number;
maxPrice: number;
}
interface AdminDashboardPayload {
metrics: {
totalPending: number;
inReview: number;
categoriesTotal: number;
newsTotal: number;
placesTotal: number;
};
reviewQueue: AdminQueueItem[];
updateQueue: AdminQueueItem[];
}
function getToken() {
if (typeof window === "undefined") return "";
return sessionStorage.getItem("token") || localStorage.getItem("token") || "";
}
function formatPrice(min: number, max: number) {
const formatter = new Intl.NumberFormat("id-ID");
if (!min && !max) return "—";
if (min === max || !max) return `Rp ${formatter.format(min || max)}`;
return `Rp ${formatter.format(min)} - Rp ${formatter.format(max)}`;
}
export default function AdminDashboardPage() {
const [data, setData] = useState<AdminDashboardPayload | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function load() {
setLoading(true);
setError("");
try {
const res = await fetch("/api/dashboard/admin", {
headers: { "x-auth-token": getToken() },
});
const result = await res.json();
if (!res.ok) {
throw new Error(getBackendErrorMessage(result, "Failed to load admin dashboard"));
}
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load admin dashboard");
} finally {
setLoading(false);
}
}
load();
}, []);
return (
<>
<div className="mb-12 flex flex-col md:flex-row md:items-end justify-between gap-6">
<div className="max-w-2xl">
<h2 className="text-4xl font-extrabold tracking-tight text-on-surface mb-2">Curator Intelligence</h2>
<p className="text-slate-500 text-lg leading-relaxed">
Central oversight for global inventory. Reviewing market listings and vendor updates for the Ina Trading ecosystem.
</p>
</div>
<div className="flex items-center gap-4">
<div className="bg-primary-container text-on-primary-container px-4 py-2 rounded-lg flex items-center gap-2">
<span className="material-symbols-outlined text-sm">bolt</span>
<span className="text-xs font-bold uppercase tracking-widest">
{loading ? "Loading..." : `${data?.metrics.categoriesTotal ?? 0} Categories Active`}
</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
{[
{ label: "Total Pending", value: String(data?.metrics.totalPending ?? 0), sub: `${data?.metrics.newsTotal ?? 0} news`, subColor: "text-primary" },
{ label: "In Review", value: String(data?.metrics.inReview ?? 0), sub: `${data?.metrics.placesTotal ?? 0} places`, subColor: "text-tertiary" },
{ label: "Categories", value: String(data?.metrics.categoriesTotal ?? 0), sub: "taxonomy live", subColor: "text-slate-400", valueColor: "text-tertiary" },
].map((metric) => (
<div key={metric.label} className="bg-surface-container-lowest p-8 rounded-xl relative overflow-hidden shadow-[0px_20px_40px_rgba(25,28,30,0.06)] group">
<div className="absolute top-0 right-0 w-32 h-32 bg-slate-50 rounded-bl-full -mr-8 -mt-8 transition-transform group-hover:scale-110" />
<p className="text-[10px] uppercase tracking-widest text-slate-400 mb-2 relative z-10">{metric.label}</p>
<div className="flex items-baseline gap-2 relative z-10">
<h3 className={`text-5xl font-black ${metric.valueColor || "text-on-surface"}`}>{loading ? "—" : metric.value}</h3>
<span className={`text-sm font-bold ${metric.subColor}`}>{metric.sub}</span>
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
<div className="lg:col-span-7">
<div className="flex items-center justify-between mb-6">
<div>
<h4 className="text-2xl font-bold tracking-tight">New Product Review Queue</h4>
<p className="text-sm text-slate-400 uppercase tracking-widest mt-1">Initial Listing Requests</p>
</div>
<Link href="/admin/review" className="text-sm font-semibold text-primary hover:underline">View All</Link>
</div>
<div className="bg-surface-container-low rounded-xl overflow-hidden p-1">
<div className="bg-surface-container-lowest rounded-lg">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-50">
{["Product Name", "Vendor", "Request Date", "Action"].map((h) => (
<th key={h} className={`px-6 py-4 text-[10px] font-bold uppercase tracking-[0.1em] text-slate-400 ${h === "Action" ? "text-right" : ""}`}>
{h}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{(data?.reviewQueue || []).map((item) => (
<tr key={item.id} className="group hover:bg-slate-50 transition-colors">
<td className="px-6 py-5">
<div className="flex items-center">
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center mr-4 flex-shrink-0">
<span className="material-symbols-outlined text-slate-400">inventory_2</span>
</div>
<span className="font-bold text-on-surface">{item.name}</span>
</div>
</td>
<td className="px-6 py-5 text-slate-600 font-medium">{item.seller}</td>
<td className="px-6 py-5 text-slate-500 font-medium">{item.date}</td>
<td className="px-6 py-5 text-right">
<Link href={`/admin/review/${item.id}`} className="inline-block bg-gradient-to-r from-primary to-primary-container text-white px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest shadow-lg shadow-primary/20 transform group-hover:scale-105 transition-all">
Take Review
</Link>
</td>
</tr>
))}
{!loading && !error && (data?.reviewQueue || []).length === 0 ? (
<tr>
<td colSpan={4} className="px-6 py-8 text-center text-sm font-semibold text-slate-400">
No pending review items.
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</div>
</div>
<div className="lg:col-span-5">
<div className="flex items-center justify-between mb-6">
<div>
<h4 className="text-2xl font-bold tracking-tight">Update Queue</h4>
<p className="text-sm text-slate-400 uppercase tracking-widest mt-1">Metadata &amp; Pricing Refinement</p>
</div>
</div>
<div className="space-y-4">
{(data?.updateQueue || []).map((item) => (
<div key={item.id} className={`bg-surface-container-lowest p-6 rounded-xl shadow-[0px_20px_40px_rgba(25,28,30,0.06)] border-l-4 ${item.note ? "border-tertiary" : "border-primary"}`}>
<div className="flex justify-between items-start mb-4">
<div>
<span className={`text-[10px] font-black uppercase tracking-widest px-2 py-1 rounded mb-2 inline-block ${item.note ? "text-tertiary bg-tertiary-fixed" : "text-primary bg-primary-fixed"}`}>
{item.market || "Update"}
</span>
<h5 className="text-lg font-bold">{item.name}</h5>
<p className="text-xs text-slate-400 font-medium">Vendor: {item.seller}</p>
</div>
<p className="text-[10px] font-bold text-slate-400 uppercase">{item.date}</p>
</div>
<div className="flex items-center justify-between pt-4 border-t border-slate-50">
<p className="text-xs text-slate-500 font-medium">{item.note || formatPrice(item.minPrice, item.maxPrice)}</p>
<Link href={`/admin/review/${item.id}`} className="text-xs font-black uppercase tracking-[0.1em] text-primary hover:text-primary-container transition-colors">
Review Changes
</Link>
</div>
</div>
))}
{!loading && !error && (data?.updateQueue || []).length === 0 ? (
<div className="bg-surface-container-lowest p-6 rounded-xl shadow-[0px_20px_40px_rgba(25,28,30,0.06)] text-sm font-semibold text-slate-400">
No update queue items available.
</div>
) : null}
<div className="mt-4 bg-surface-container-low p-6 rounded-xl">
<h6 className="text-xs font-black uppercase tracking-[0.2em] text-slate-500 mb-6">Recent Curator Actions</h6>
<div className="space-y-6 relative">
<div className="absolute left-[11px] top-2 bottom-2 w-[2px] bg-slate-200" />
{[
{
color: "bg-tertiary-fixed",
dot: "bg-tertiary",
text: `${data?.metrics.newsTotal ?? 0} news articles currently published`,
by: "Editorial data",
},
{
color: "bg-primary-fixed",
dot: "bg-primary",
text: `${data?.metrics.placesTotal ?? 0} places indexed for admin`,
by: "Location registry",
},
].map((action) => (
<div key={action.text} className="relative pl-8">
<div className={`absolute left-0 top-1 w-6 h-6 rounded-full ${action.color} flex items-center justify-center border-4 border-surface-container-low`}>
<div className={`w-2 h-2 ${action.dot} rounded-full`} />
</div>
<p className="text-sm font-bold">{action.text}</p>
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-widest">{action.by}</p>
</div>
))}
</div>
</div>
</div>
</div>
</div>
{error ? (
<div className="mt-6 rounded-xl bg-error-container px-6 py-4 text-sm font-semibold text-on-error-container">
{error}
</div>
) : null}
<div className="mt-20 bg-slate-900 text-white rounded-2xl p-10 flex flex-col md:flex-row items-center justify-between gap-8 relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/20 blur-[100px] -mr-32 -mt-32" />
<div className="relative z-10 max-w-xl">
<h4 className="text-3xl font-black mb-4">Market Saturation Report</h4>
<p className="text-slate-400 font-medium">
Current pending review load is{" "}
<span className="text-white">{data?.metrics.totalPending ?? 0}</span> items across products, places, and content workflows.
</p>
</div>
<div className="relative z-10">
<Link href="/admin/review" className="bg-white text-slate-950 font-black px-8 py-4 rounded-lg uppercase tracking-widest text-sm hover:bg-primary hover:text-white transition-all">
Open Review Queue
</Link>
</div>
</div>
</>
);
}