"use client"; import { useEffect, useState } from "react"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; type Merchant = { id: number; name: string; }; type Mapping = { id: number | null; // might be null if just created client-side merchantId: number; merchantName: string; rawCategory: string; mappedPartRole: string | null; }; type UpsertRequest = { merchantId: number; rawCategory: string; mappedPartRole: string | null; }; // Keep this in sync with your backend `partRole` values const PART_ROLE_OPTIONS = [ { value: "", label: "— Unmapped —" }, { value: "upper-receiver", label: "Upper Receiver" }, { value: "barrel", label: "Barrel" }, { value: "handguard", label: "Handguard" }, { value: "charging-handle", label: "Charging Handle" }, { value: "buffer-kit", label: "Buffer Kit" }, { value: "lower-parts-kit", label: "Lower Parts Kit" }, { value: "sight", label: "Sight / Optic" }, { value: "lower-receiver", label: "Lower Receiver" }, { value: "optic", label: "Optic" }, { value: "stock", label: "Stock" }, { value: "other", label: "Other / Ignore" }, ]; export default function CategoryMappingsPage() { const [merchants, setMerchants] = useState([]); const [selectedMerchantId, setSelectedMerchantId] = useState( null, ); const [mappings, setMappings] = useState([]); const [loadingMappings, setLoadingMappings] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); // Load merchants useEffect(() => { async function fetchMerchants() { try { const res = await fetch(`${API_BASE_URL}/admin/merchants`); if (!res.ok) { throw new Error(`Failed to load merchants (${res.status})`); } const data: Merchant[] = await res.json(); setMerchants(data); // Auto-select the first merchant if you want if (data.length > 0 && selectedMerchantId === null) { setSelectedMerchantId(data[0].id); } } catch (err: any) { console.error(err); setError(err.message ?? "Failed to load merchants"); } } fetchMerchants(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Load mappings for selected merchant useEffect(() => { if (!selectedMerchantId) return; const controller = new AbortController(); async function fetchMappings() { try { setLoadingMappings(true); setError(null); setSuccessMessage(null); const url = `${API_BASE_URL}/admin/merchant-category-mappings?merchantId=${selectedMerchantId}`; const res = await fetch(url, { signal: controller.signal }); if (!res.ok) { throw new Error(`Failed to load mappings (${res.status})`); } const data = (await res.json()) as Mapping[]; setMappings(data); } catch (err: any) { if (err.name === "AbortError") return; console.error(err); setError(err.message ?? "Failed to load mappings"); } finally { setLoadingMappings(false); } } fetchMappings(); return () => controller.abort(); }, [selectedMerchantId]); const handleChangePartRole = (rawCategory: string, newRole: string) => { setMappings((prev) => prev.map((m) => m.rawCategory === rawCategory ? { ...m, mappedPartRole: newRole || null } : m, ), ); }; const handleSave = async () => { if (!selectedMerchantId) return; setSaving(true); setError(null); setSuccessMessage(null); try { // Save each mapping in parallel const requests: Promise[] = mappings.map((m) => { const payload: UpsertRequest = { merchantId: selectedMerchantId, rawCategory: m.rawCategory, mappedPartRole: m.mappedPartRole, }; return fetch(`${API_BASE_URL}/admin/merchant-category-mappings`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); }); const responses = await Promise.all(requests); const failed = responses.filter((r) => !r.ok); if (failed.length > 0) { throw new Error(`Some mappings failed to save (${failed.length})`); } setSuccessMessage("Mappings saved. Re-run the import to apply changes."); } catch (err: any) { console.error(err); setError(err.message ?? "Failed to save mappings"); } finally { setSaving(false); } }; const selectedMerchantName = merchants.find((m) => m.id === selectedMerchantId)?.name ?? "—"; return (

Shadow Standard / Admin

Merchant Category Mapping

Normalize each merchant's category names into your internal part roles (upper, barrel, handguard, etc.). Unmapped categories will be skipped during import until they are mapped here.

{error && (
{error}
)} {successMessage && (
{successMessage}
)} {!selectedMerchantId ? (

Choose a merchant above to view category mappings.

) : loadingMappings ? (

Loading mappings for {selectedMerchantName}

) : mappings.length === 0 ? (

No category rows yet for this merchant. Once you run an import, discovered categories will appear here automatically.

) : ( <>
Mappings for {selectedMerchantName}.{" "} Set a part role for each raw category. Unmapped categories will be skipped by the importer.
{mappings.map((m) => ( ))}
Raw Category Mapped Part Role
{m.rawCategory}
)}
); }