"use client"; import { useEffect, useMemo, useState } from "react"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; type PendingBucket = { merchantId: number; merchantName: string; rawCategoryKey: string; mappedPartRole: string | null; productCount: number; }; /** * Canonical part roles (kebab-case) — should match what the importer normalizes to. * Keep this list tight + intentional so mappings don't create junk roles in the DB. */ const PART_ROLE_OPTIONS = [ // Assemblies / receivers "upper-receiver", "complete-upper", "lower-receiver", "complete-lower", // Upper sub-parts "bcg", "barrel", "gas-block", "gas-tube", "muzzle-device", "suppressor", "handguard", "charging-handle", // Lower sub-parts "lower-parts", "trigger", "grip", "safety", "buffer", "stock", // Sights / optics "sights", "optic", // Accessories / gear "magazine", "weapon-light", "foregrip", "bipod", "sling", "rail-accessory", "tools", ] as const; type PartRole = (typeof PART_ROLE_OPTIONS)[number]; function toLabel(role: string) { // "complete-upper" -> "Complete Upper" return role .split("-") .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); } export default function MappingAdminPage() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [savingKey, setSavingKey] = useState(null); const [error, setError] = useState(null); const options = useMemo( () => PART_ROLE_OPTIONS.map((value) => ({ value, label: toLabel(value), })), [] ); useEffect(() => { async function load() { try { setLoading(true); setError(null); const res = await fetch( `${API_BASE_URL}/api/admin/mapping/pending-buckets`, { headers: { Accept: "application/json" } } ); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error( `Failed to load pending buckets (${res.status})${text ? `: ${text}` : ""}` ); } const data: PendingBucket[] = await res.json(); setRows(data); } catch (e: any) { console.error(e); setError(e.message ?? "Failed to load pending buckets"); } finally { setLoading(false); } } load(); }, []); const handleChangeRole = (idx: number, value: string) => { // Always store normalized kebab-case (defensive) const normalized = value ? value.trim().toLowerCase().replace(/_/g, "-") : ""; setRows((prev) => { const next = [...prev]; next[idx] = { ...next[idx], mappedPartRole: normalized || null }; return next; }); }; const handleSave = async (row: PendingBucket) => { if (!row.mappedPartRole) return; const mapped = row.mappedPartRole .trim() .toLowerCase() .replace(/_/g, "-"); // Basic guard: prevent saving random roles from stale data const allowed = new Set(PART_ROLE_OPTIONS as readonly string[]); if (!allowed.has(mapped)) { setError( `Refusing to save unknown part role: "${row.mappedPartRole}". Pick a role from the dropdown list.` ); return; } const key = `${row.merchantId}-${row.rawCategoryKey}`; try { setSavingKey(key); setError(null); const res = await fetch(`${API_BASE_URL}/api/admin/mapping/apply`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ merchantId: row.merchantId, rawCategoryKey: row.rawCategoryKey, mappedPartRole: mapped, }), }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error( `Failed to save mapping (${res.status})${text ? `: ${text}` : ""}` ); } // After save, remove this bucket from the list setRows((prev) => prev.filter( (r) => !( r.merchantId === row.merchantId && r.rawCategoryKey === row.rawCategoryKey ) ) ); } catch (e: any) { console.error(e); setError(e.message ?? "Failed to save mapping"); } finally { setSavingKey(null); } }; return (

Bulk Mapping – Pending Categories

Bucketed by merchant + raw category. Map each bucket to a part role to clean up the builder catalog.

{loading && (

Loading pending buckets…

)} {error && (

Error: {error}

)} {!loading && rows.length === 0 && !error && (

No pending mapping buckets 🎉

)} {!loading && rows.length > 0 && (

Pending Buckets

{rows.length} buckets •{" "} {rows.reduce((s, r) => s + r.productCount, 0)} products
{rows.map((row, idx) => { const rowKey = `${row.merchantId}-${row.rawCategoryKey}`; const isSaving = savingKey === rowKey; return ( ); })}
Merchant Raw Category Products Part Role Action
{row.merchantName} {row.rawCategoryKey} {row.productCount}
)}
); }