From 9e5b86967b01c8cb025d50bc696e248aed66af10 Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 7 Dec 2025 16:18:28 -0500 Subject: [PATCH] new admin dashbioard and mapping page --- app/admin/import-status/page.tsx | 322 ++++++++++++++++++++++--------- app/admin/page.tsx | 221 +++++++++++++++------ 2 files changed, 401 insertions(+), 142 deletions(-) diff --git a/app/admin/import-status/page.tsx b/app/admin/import-status/page.tsx index 31e4a6c..c3d44ed 100644 --- a/app/admin/import-status/page.tsx +++ b/app/admin/import-status/page.tsx @@ -1,111 +1,259 @@ "use client"; import { useEffect, useState } from "react"; - -type ImportStatus = "RAW" | "PENDING_MAPPING" | "MAPPED"; - -type SummaryRow = { - status: ImportStatus; - count: number; -}; - -type ByMerchantRow = { - merchantName: string; - platform: string; - status: ImportStatus; - count: number; -}; +import Link from "next/link"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; +const formatCount = (value: number | null | undefined) => + (typeof value === "number" ? value : 0).toLocaleString(); + +type ImportSummary = { + totalProducts: number; + mappedProducts: number; + pendingProducts: number; +}; + +type RawByMerchantRow = { + merchantId: number; + merchantName: string; + platform: string; + status: string; + count: number; +}; + +type MerchantImportRow = { + merchantId: number; + merchantName: string; + totalProducts: number; + mappedProducts: number; + pendingProducts: number; + lastImportAt?: string | null; +}; + export default function ImportStatusPage() { - const [summary, setSummary] = useState([]); - const [byMerchant, setByMerchant] = useState([]); + const [summary, setSummary] = useState(null); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [importingId, setImportingId] = useState(null); useEffect(() => { async function load() { - const [summaryRes, byMerchantRes] = await Promise.all([ - fetch(`${API_BASE_URL}/api/admin/import-status/summary`), - fetch(`${API_BASE_URL}/api/admin/import-status/by-merchant`), - ]); + try { + setLoading(true); + setError(null); - setSummary(await summaryRes.json()); - setByMerchant(await byMerchantRes.json()); + const [summaryRes, byMerchantRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/admin/import-status/summary`), + fetch(`${API_BASE_URL}/api/admin/import-status/by-merchant`), + ]); + + if (!summaryRes.ok || !byMerchantRes.ok) { + throw new Error("Failed to load import status"); + } + + const summaryJson: ImportSummary = await summaryRes.json(); + const byMerchantRaw: RawByMerchantRow[] = await byMerchantRes.json(); + + // Aggregate per merchant using merchantId as the key + const merchantMap = new Map(); + + for (const row of byMerchantRaw) { + const id = row.merchantId; + const name = row.merchantName; + const status = row.status; + const count = row.count ?? 0; + + if (!merchantMap.has(id)) { + merchantMap.set(id, { + merchantId: id, + merchantName: name, + totalProducts: 0, + mappedProducts: 0, + pendingProducts: 0, + lastImportAt: null, // not provided by API yet + }); + } + + const agg = merchantMap.get(id)!; + agg.totalProducts += count; + + if (status === "MAPPED") { + agg.mappedProducts += count; + } else if (status === "PENDING_MAPPING") { + agg.pendingProducts += count; + } + } + + const byMerchantJson: MerchantImportRow[] = Array.from( + merchantMap.values() + ); + + setSummary(summaryJson); + setRows(byMerchantJson); + } catch (e: any) { + console.error(e); + setError(e.message ?? "Failed to load import status"); + } finally { + setLoading(false); + } } load(); }, []); - const total = summary.reduce((sum, row) => sum + row.count, 0); + async function handleReimport(merchantId: number) { + try { + setImportingId(merchantId); + const res = await fetch( + `${API_BASE_URL}/api/admin/merchants/${merchantId}/import`, + { + method: "POST", + } + ); + + if (!res.ok) { + throw new Error(`Re-import failed (${res.status})`); + } + + // Optionally re-load data here if you want the table to refresh automatically + } catch (e: any) { + console.error(e); + alert(e.message ?? "Failed to trigger import"); + } finally { + setImportingId(null); + } + } return (
-
-

- Import Status Dashboard -

-

- Quick sanity check on how clean (or not) your catalog is. -

- - {/* Status cards */} -
- {summary.map((row) => ( -
-
- {row.status} -
-
- {row.count} -
-
- {total > 0 - ? `${((row.count / total) * 100).toFixed(1)}% of catalog` - : "—"} -
-
- ))} -
- - {/* Merchant breakdown */} -
-

- By Merchant & Platform -

-
- - - - - - - - - - - {byMerchant.map((row, i) => ( - - - - - - - ))} - -
MerchantPlatformStatusCount
- {row.merchantName} - {row.platform}{row.status} - {row.count} -
+
+
+
+

+ Import Status +

+

+ Per-merchant feed health and mapping coverage. +

-
+ + ← Back to Admin Home + + + + {loading && ( +

Loading import status…

+ )} + + {error &&

Error: {error}

} + + {summary && !loading && !error && ( + <> + {/* Summary cards */} +
+
+

+ Total Products +

+

+ {formatCount(summary.totalProducts)} +

+
+
+

+ Mapped +

+

+ {formatCount(summary.mappedProducts)} +

+
+
+

+ Pending Mapping +

+

+ {formatCount(summary.pendingProducts)} +

+
+
+ +
+

+ By Merchant +

+ +
+ + + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + + ))} + +
MerchantTotalMappedPendingLast ImportActions
+ + {row.merchantName} + + + {formatCount(row.totalProducts)} + + {formatCount(row.mappedProducts)} + + {formatCount(row.pendingProducts)} + + {row.lastImportAt + ? new Date(row.lastImportAt).toLocaleString() + : "—"} + +
+ + Mapping + + + +
+
+
+
+ + )}
); diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 6fb7bee..44441f2 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,67 +1,178 @@ "use client"; +import { useEffect, useState } from "react"; import Link from "next/link"; -import { useAuth } from "@/context/AuthContext"; -export default function AdminHomePage() { - const { token } = useAuth(); +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; - if (!token) { - return ( -
- You must be logged in to view this page. -
- ); - } +type AdminOverview = { + totalProducts: number; + mappedProducts: number; + unmappedProducts: number; + merchantCount: number; + categoryMappingCount: number; +}; - const cards = [ - { - title: "Canonical categories", - href: "admin/categories", - description: - "Manage the core builder categories and their group/grouping + sort order.", - }, - { - title: "Part role mappings", - href: "/admin/part-role-mappings", - description: - "Advanced: map logical builder part roles to canonical categories per platform.", - }, - { - title: "Merchant category mappings", - href: "/admin/merchant-category-mappings", - description: - "Map raw merchant feed categories to your canonical categories.", - }, - ]; +export default function AdminLandingPage() { + const [overview, setOverview] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function load() { + try { + setLoading(true); + setError(null); + const res = await fetch(`${API_BASE_URL}/api/admin/dashboard/overview`); + if (!res.ok) { + throw new Error(`Failed to load dashboard (${res.status})`); + } + const data: AdminOverview = await res.json(); + setOverview(data); + } catch (e: any) { + console.error(e); + setError(e.message ?? "Failed to load admin dashboard"); + } finally { + setLoading(false); + } + } + + load(); + }, []); return ( -
-
-

- Admin -

-

- Internal tools to keep your builder data clean and consistent. -

-
- -
- {cards.map((card) => ( - -

- {card.title} -

-

- {card.description} +

+
+
+
+

+ Battl Builder – Admin +

+

+ Control room for feeds, mappings, and catalog health.

- - ))} +
+
+ + {loading && ( +

Loading dashboard…

+ )} + + {error && ( +

Error: {error}

+ )} + + {overview && !loading && !error && ( + <> + {/* Stats row */} +
+
+

+ Total Products +

+

+ {overview.totalProducts.toLocaleString()} +

+
+ +
+

+ Mapped Products +

+

+ {overview.mappedProducts.toLocaleString()} +

+
+ +
+

+ Unmapped Products +

+

+ {overview.unmappedProducts.toLocaleString()} +

+

+ Products still in PENDING_MAPPING. +

+
+ +
+

+ Merchants / Mappings +

+

+ {overview.merchantCount}{" "} + + merchants + +

+

+ {overview.categoryMappingCount.toLocaleString()} mappings +

+
+
+ + {/* Nav cards */} +
+

+ Admin Tools +

+
+ {/* Bulk Mapping */} + +

+ Bulk Category Mapping +

+

+ Bucket raw merchant categories and map them to part roles to + keep the catalog clean. +

+

+ {overview.unmappedProducts.toLocaleString()} products + pending mapping +

+ + + {/* Import Status */} + +

+ Import Status +

+

+ Inspect last feed runs, per-merchant health, and errors. +

+

+ View import summaries  → +

+ + + {/* Placeholder for future admin tools */} + +

+ Catalog Explorer +

+

+ (Future) Browse normalized products, offers, and compatibility. +

+

+ Coming soon +

+ +
+
+ + )}
-
+ ); } \ No newline at end of file