"use client"; import { useEffect, 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; }; const PART_ROLE_OPTIONS: string[] = [ // LOWER – receivers "LOWER_RECEIVER_STRIPPED", "LOWER_RECEIVER_COMPLETE", "LOWER_RECEIVER_COMPLETE_PISTOL", "LOWER_RECEIVER_COMPLETE_RIFLE", "LOWER_RECEIVER_80", "LOWER_RECEIVER_BILLET", "LOWER_RECEIVER_FORGED", "LOWER_RECEIVER_POLYMER", // LOWER – internals & controls "LOWER_PARTS_KIT", "LOWER_PARTS_KIT_ENHANCED", "TRIGGER", "TRIGGER_OTHER", "TRIGGER_COMBAT", "TRIGGER_MATCH", "TRIGGER_DROP_IN", "TRIGGER_SINGLE_STAGE", "TRIGGER_TWO_STAGE", "TRIGGER_GUARD", "PISTOL_GRIP", "PISTOL_GRIP_ERGO", "PISTOL_GRIP_VERTICAL", "SAFETY_SELECTOR", "SAFETY_SELECTOR_AMBI", "SAFETY_SELECTOR_45_DEG", "MAG_RELEASE", "TAKEDOWN_PINS", "SPRINGS_PINS_MISC", // LOWER – buffer & stock "BUFFER_KIT", "BUFFER_TUBE", "BUFFER_TUBE_MILSPEC", "BUFFER_TUBE_COMMERCIAL", "BUFFER_SPRING", "BUFFER_WEIGHT", "STOCK_ADJUSTABLE", "STOCK_FIXED", "STOCK_FOLDER", "STOCK_PRECISION", "BRACE_PISTOL", // UPPER – receivers & BCG "UPPER_RECEIVER_STRIPPED", "UPPER_RECEIVER_BILLET", "UPPER_RECEIVER_MONOLITHIC", "UPPER_RECEIVER_COMPLETE", "UPPER_RECEIVER_COMPLETE_PISTOL", "UPPER_RECEIVER_COMPLETE_RIFLE", "BOLT_CARRIER_GROUP", "BCG_COMPLETE", "BCG_LIGHTWEIGHT", "BCG_NICKEL_BORON", // UPPER – barrel & gas "BARREL", "BARREL_THREADED", "BARREL_PENCIL", "BARREL_MATCH", "BARREL_HEAVY", "BARREL_14_5_PINNED", "GAS_BLOCK", "GAS_BLOCK_ADJUSTABLE", "GAS_BLOCK_LOW_PROFILE", "GAS_TUBE_PISTOL", "GAS_TUBE_CAR", "GAS_TUBE_MID", "GAS_TUBE_RIFLE", // UPPER – front end / muzzle "HANDGUARD_MLOK", "HANDGUARD_KEYMOD", "HANDGUARD_QUAD", "HANDGUARD_SLICK", "MUZZLE_BRAKE", "MUZZLE_COMPENSATOR", // 👇 generic / backup muzzle roles "MUZZLE_DEVICE", "MUZZLE_DEVICE_OTHER", "FLASH_HIDER", "MUZZLE_DEVICE_QD_MOUNT", "SUPPRESSOR_DIRECT_THREAD", "SUPPRESSOR_QD", "SUPPRESSOR_RIFLE", "SUPPRESSOR_PISTOL", "CHARGING_HANDLE", "CHARGING_HANDLE_AMBI", "CHARGING_HANDLE_GAS_BUSTER", // Sights / optics "SIGHTS_BACKUP", "SIGHTS_IRON_FIXED", "SIGHTS_IRON_FLIP", "SIGHTS_OFFSET", "OPTIC_REDDOT", "OPTIC_LPVO", "OPTIC_HOLOGRAPHIC", "OPTIC_PRISM", "OPTIC_MAGNIFIER", // Accessories "MAGAZINE", "SLING", "BIPOD", "MOUNT_SCOPE", "MOUNT_OFFSET", "LIGHT_WEAPON", "LASER_VISIBLE", "LASER_IR", "TOOL", ]; export default function MappingAdminPage() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [savingKey, setSavingKey] = useState(null); const [error, setError] = useState(null); useEffect(() => { async function load() { try { setLoading(true); setError(null); const res = await fetch( `${API_BASE_URL}/api/admin/mapping/pending-buckets` ); if (!res.ok) { throw new Error(`Failed to load pending buckets (${res.status})`); } 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) => { setRows((prev) => { const next = [...prev]; next[idx] = { ...next[idx], mappedPartRole: value || null }; return next; }); }; const handleSave = async (row: PendingBucket) => { if (!row.mappedPartRole) 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: row.mappedPartRole, }), }); if (!res.ok) { throw new Error(`Failed to save mapping (${res.status})`); } // 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}
)}
); }