Refine seller onboarding and product review flows
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useLanguage } from "@/lib/i18n-context";
|
||||
|
||||
interface AnalyticsPoint {
|
||||
@ -31,6 +32,25 @@ interface SellerDashboardPayload {
|
||||
recentOrders: DashboardOrder[];
|
||||
}
|
||||
|
||||
interface ProductSearchRow {
|
||||
id: string;
|
||||
name: string;
|
||||
market?: string | null;
|
||||
state?: string | null;
|
||||
status?: string | null;
|
||||
totalStock?: number | null;
|
||||
}
|
||||
|
||||
interface WarehouseSearchRow {
|
||||
id: string;
|
||||
name: string | null;
|
||||
address: string | null;
|
||||
city: string | null;
|
||||
province: string | null;
|
||||
country: string | null;
|
||||
warehouseType: string | null;
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
if (typeof window === "undefined") return "";
|
||||
return sessionStorage.getItem("token") || localStorage.getItem("token") || "";
|
||||
@ -52,10 +72,14 @@ function statusColor(status: string) {
|
||||
return "text-primary bg-primary/10";
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
function DashboardContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useLanguage();
|
||||
const d = t.dashboard.overview;
|
||||
const [data, setData] = useState<SellerDashboardPayload | null>(null);
|
||||
const [productResults, setProductResults] = useState<ProductSearchRow[]>([]);
|
||||
const [warehouseResults, setWarehouseResults] = useState<WarehouseSearchRow[]>([]);
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@ -84,8 +108,101 @@ export default function DashboardPage() {
|
||||
|
||||
const analytics = data?.analytics || [];
|
||||
const maxAnalytics = Math.max(...analytics.map((item) => item.value), 1);
|
||||
const totalRevenue = (data?.recentOrders || []).reduce((sum, item) => sum + item.amount, 0);
|
||||
const totalOrders = data?.recentOrders.length || 0;
|
||||
const rawDashboardQuery = (searchParams.get("q") || "").trim();
|
||||
const dashboardQuery = rawDashboardQuery.toLowerCase();
|
||||
const filteredOrders = (data?.recentOrders || []).filter((order) => {
|
||||
if (!dashboardQuery) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [
|
||||
order.product,
|
||||
order.sku,
|
||||
order.customer,
|
||||
order.location,
|
||||
order.date,
|
||||
order.status,
|
||||
String(order.amount),
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(dashboardQuery);
|
||||
});
|
||||
const totalRevenue = filteredOrders.reduce((sum, item) => sum + item.amount, 0);
|
||||
const totalOrders = filteredOrders.length;
|
||||
|
||||
useEffect(() => {
|
||||
async function loadSearchResults() {
|
||||
if (!dashboardQuery) {
|
||||
setProductResults([]);
|
||||
setWarehouseResults([]);
|
||||
setSearchLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchLoading(true);
|
||||
|
||||
try {
|
||||
const [productsRes, warehousesRes] = await Promise.all([
|
||||
fetch("/api/products?page=1&size=100", {
|
||||
headers: { "x-auth-token": getToken() },
|
||||
}),
|
||||
fetch("/api/products/warehouses?page=1&size=100", {
|
||||
headers: { "x-auth-token": getToken() },
|
||||
}),
|
||||
]);
|
||||
|
||||
const [productsResult, warehousesResult] = await Promise.all([
|
||||
productsRes.json().catch(() => ({})),
|
||||
warehousesRes.json().catch(() => ({})),
|
||||
]);
|
||||
|
||||
const products: ProductSearchRow[] = Array.isArray(productsResult?.rows)
|
||||
? productsResult.rows
|
||||
: Array.isArray(productsResult?.data?.rows)
|
||||
? productsResult.data.rows
|
||||
: [];
|
||||
|
||||
const warehouses: WarehouseSearchRow[] = Array.isArray(warehousesResult?.rows)
|
||||
? warehousesResult.rows
|
||||
: Array.isArray(warehousesResult?.data)
|
||||
? warehousesResult.data
|
||||
: [];
|
||||
|
||||
setProductResults(
|
||||
products.filter((item) =>
|
||||
[item.name, item.market, item.state, item.status, String(item.totalStock ?? "")]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(dashboardQuery)
|
||||
)
|
||||
);
|
||||
|
||||
setWarehouseResults(
|
||||
warehouses.filter((item) =>
|
||||
[
|
||||
item.name || "",
|
||||
item.address || "",
|
||||
item.city || "",
|
||||
item.province || "",
|
||||
item.country || "",
|
||||
item.warehouseType || "",
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(dashboardQuery)
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
setProductResults([]);
|
||||
setWarehouseResults([]);
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadSearchResults();
|
||||
}, [dashboardQuery]);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
@ -96,6 +213,101 @@ export default function DashboardPage() {
|
||||
<p className="text-on-surface-variant font-medium">{d.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{dashboardQuery ? (
|
||||
<div className="mb-10 rounded-2xl border border-surface-container bg-surface-container-lowest p-8 magazine-shadow">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.22em] text-primary">
|
||||
Dashboard Search
|
||||
</p>
|
||||
<h2 className="mt-2 text-2xl font-black tracking-tight text-on-surface">
|
||||
Hasil pencarian untuk "{rawDashboardQuery}"
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-on-surface-variant">
|
||||
{searchLoading
|
||||
? "Mencari data..."
|
||||
: `${filteredOrders.length} order, ${productResults.length} produk, ${warehouseResults.length} warehouse`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-6 lg:grid-cols-3">
|
||||
<div className="rounded-2xl bg-surface p-5">
|
||||
<h3 className="text-sm font-black uppercase tracking-[0.18em] text-outline">
|
||||
Orders
|
||||
</h3>
|
||||
<div className="mt-4 space-y-3">
|
||||
{filteredOrders.slice(0, 4).map((order) => (
|
||||
<div key={order.id} className="rounded-xl border border-surface-container p-4">
|
||||
<p className="font-bold text-on-surface">{order.product}</p>
|
||||
<p className="mt-1 text-sm text-on-surface-variant">{order.customer} · {order.location}</p>
|
||||
</div>
|
||||
))}
|
||||
{!searchLoading && filteredOrders.length === 0 ? (
|
||||
<p className="text-sm font-medium text-on-surface-variant">Tidak ada order yang cocok.</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl bg-surface p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-black uppercase tracking-[0.18em] text-outline">
|
||||
Products
|
||||
</h3>
|
||||
<a href="/products" className="text-xs font-bold text-primary hover:underline">
|
||||
Lihat semua
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{productResults.slice(0, 4).map((product) => (
|
||||
<a
|
||||
key={product.id}
|
||||
href="/products"
|
||||
className="block rounded-xl border border-surface-container p-4 transition-colors hover:bg-surface-container-low"
|
||||
>
|
||||
<p className="font-bold text-on-surface">{product.name}</p>
|
||||
<p className="mt-1 text-sm text-on-surface-variant">
|
||||
{product.market || product.state || product.status || "Produk"}
|
||||
</p>
|
||||
</a>
|
||||
))}
|
||||
{!searchLoading && productResults.length === 0 ? (
|
||||
<p className="text-sm font-medium text-on-surface-variant">Tidak ada produk yang cocok.</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl bg-surface p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-black uppercase tracking-[0.18em] text-outline">
|
||||
Warehouses
|
||||
</h3>
|
||||
<a href="/dashboard/warehouse" className="text-xs font-bold text-primary hover:underline">
|
||||
Lihat semua
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{warehouseResults.slice(0, 4).map((warehouse) => (
|
||||
<a
|
||||
key={warehouse.id}
|
||||
href="/dashboard/warehouse"
|
||||
className="block rounded-xl border border-surface-container p-4 transition-colors hover:bg-surface-container-low"
|
||||
>
|
||||
<p className="font-bold text-on-surface">{warehouse.name || "Tanpa nama"}</p>
|
||||
<p className="mt-1 text-sm text-on-surface-variant">
|
||||
{[warehouse.city, warehouse.province, warehouse.country].filter(Boolean).join(", ") || warehouse.address || "Warehouse"}
|
||||
</p>
|
||||
</a>
|
||||
))}
|
||||
{!searchLoading && warehouseResults.length === 0 ? (
|
||||
<p className="text-sm font-medium text-on-surface-variant">Tidak ada warehouse yang cocok.</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-10">
|
||||
<div className="bg-surface-container-lowest p-8 rounded-xl magazine-shadow relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-primary"></div>
|
||||
@ -220,7 +432,14 @@ export default function DashboardPage() {
|
||||
|
||||
<div className="bg-surface-container-lowest rounded-xl magazine-shadow overflow-hidden">
|
||||
<div className="p-8 flex items-center justify-between border-b border-surface-container">
|
||||
<h4 className="text-xl font-black font-headline tracking-tight">{d.recentOrders}</h4>
|
||||
<div>
|
||||
<h4 className="text-xl font-black font-headline tracking-tight">{d.recentOrders}</h4>
|
||||
{dashboardQuery ? (
|
||||
<p className="mt-2 text-sm font-medium text-on-surface-variant">
|
||||
Menampilkan hasil pencarian untuk <span className="font-bold text-on-surface">"{searchParams.get("q")}"</span>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button className="text-primary font-bold text-sm hover:underline flex items-center gap-1">
|
||||
{d.viewAll}
|
||||
<span className="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
@ -239,7 +458,7 @@ export default function DashboardPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-container">
|
||||
{(data?.recentOrders || []).map((order) => (
|
||||
{filteredOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-surface-container-low/50 transition-colors">
|
||||
<td className="px-8 py-5">
|
||||
<div className="flex items-center gap-4">
|
||||
@ -270,10 +489,10 @@ export default function DashboardPage() {
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!loading && !error && (data?.recentOrders || []).length === 0 ? (
|
||||
{!loading && !error && filteredOrders.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-8 py-10 text-center text-sm font-semibold text-on-surface-variant">
|
||||
No recent orders available.
|
||||
{dashboardQuery ? "Tidak ada order yang cocok dengan pencarian Anda." : "No recent orders available."}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
@ -290,3 +509,11 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<DashboardContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user