"use client"; import { useEffect, useState } from "react"; import { useApi } from "@/lib/useApi"; type Merchant = { id: number; name: string; }; type CategoryMapping = { id: number; merchantId: number; merchantName: string; rawCategoryPath: string; partCategoryId: number | null; partCategoryName?: string | null; }; // Matches the Spring MerchantCategoryMappingDto record type MerchantCategoryMappingDto = { id: number; merchantId: number; merchantName: string; rawCategoryPath: string; partCategoryId: number | null; partCategoryName?: string | null; }; type AdminCategory = { id: number; name: string; groupName?: string | null; }; export default function MerchantCategoryMappingPage() { const { get, post } = useApi(); const [merchants, setMerchants] = useState([]); const [selectedMerchantId, setSelectedMerchantId] = useState( null ); const [mappings, setMappings] = useState([]); const [loadingMerchants, setLoadingMerchants] = useState(false); const [loadingMappings, setLoadingMappings] = useState(false); const [savingId, setSavingId] = useState(null); const [error, setError] = useState(null); const [categories, setCategories] = useState([]); const [loadingCategories, setLoadingCategories] = useState(false); const [dirty, setDirty] = useState>({}); // 1) Load merchants for the dropdown useEffect(() => { setLoadingMerchants(true); setError(null); get("/api/admin/category-mappings/merchants") .then((data) => { setMerchants(data); if (data.length > 0 && selectedMerchantId == null) { setSelectedMerchantId(data[0].id); } }) .catch((err: any) => { console.error("Failed to load merchants", err); setError( err instanceof Error ? err.message : "Failed to load merchants" ); }) .finally(() => setLoadingMerchants(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Load global (canonical) part categories for the dropdown // Load global (canonical) part categories for the dropdown useEffect(() => { setLoadingCategories(true); setError(null); get("/api/admin/categories") .then((data) => { setCategories(data); }) .catch((err: any) => { console.error("Failed to load categories", err); setError( err instanceof Error ? err.message : "Failed to load categories" ); }) .finally(() => setLoadingCategories(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 2) Load mappings when merchant changes (THIS sends merchantId) useEffect(() => { if (selectedMerchantId == null) return; setLoadingMappings(true); setError(null); get( `/api/admin/category-mappings?merchantId=${selectedMerchantId}` ) .then((data) => { setMappings(data); }) .catch((err: any) => { console.error("Failed to load category mappings", err); setError( err instanceof Error ? err.message : "Failed to load category mappings" ); }) .finally(() => setLoadingMappings(false)); }, [selectedMerchantId]); // 3) Save a single mapping row (adjust endpoint if yours differs) const handleSaveMapping = async ( mappingId: number, updated: { partCategoryId: number | null } ) => { setSavingId(mappingId); setError(null); try { const updatedMapping = await post( `/api/admin/category-mappings/${mappingId}`, { partCategoryId: updated.partCategoryId, } ); setMappings((prev) => prev.map((m) => m.id === mappingId ? { ...m, // map Java DTO fields back into your UI model merchantId: updatedMapping.merchantId, merchantName: updatedMapping.merchantName, rawCategoryPath: updatedMapping.rawCategoryPath, partCategoryId: updatedMapping.partCategoryId, partCategoryName: updatedMapping.partCategoryName, } : m ) ); } catch (err: any) { console.error("Failed to save mapping", err); setError(err instanceof Error ? err.message : "Failed to save mapping"); } finally { setSavingId(null); } }; const handleSaveAll = async () => { if (Object.keys(dirty).length === 0) return; // special flag to indicate a global save is in progress setSavingId(-1); setError(null); try { const dirtyRows = Object.values(dirty); const updatedMappings = await Promise.all( dirtyRows.map((row) => post( `/api/admin/category-mappings/${row.id}`, { partCategoryId: row.partCategoryId ?? null, } ) ) ); setMappings((prev) => prev.map((m) => { const updated = updatedMappings.find((u) => u.id === m.id); if (!updated) return m; return { ...m, merchantId: updated.merchantId, merchantName: updated.merchantName, rawCategoryPath: updated.rawCategoryPath, partCategoryId: updated.partCategoryId, partCategoryName: updated.partCategoryName, }; }) ); // Clear dirty state after successful batch save setDirty({}); } catch (err: any) { console.error("Failed to save all mappings", err); setError( err instanceof Error ? err.message : "Failed to save all mappings" ); } finally { setSavingId(null); } }; return (

Merchant Category Mapping

Merchant:
{error && (
{error}
)} {Object.keys(dirty).length > 0 && (
)}
Category Mappings
{loadingMappings ? (
Loading mappings…
) : mappings.length === 0 ? (
No mappings found for this merchant.
) : ( {mappings.map((m) => ( ))}
Merchant Category Global Category Actions
{m.rawCategoryPath} {loadingCategories ? ( Loading categories… ) : categories.length === 0 ? ( No categories ) : ( )} {m.partCategoryName && (
Current: {m.partCategoryName}
)}
)}
); }