"use client"; import { useEffect, useState } from "react"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; type MerchantAdminDto = { id: number; name: string; avantlinkMid: string; feedUrl: string; offerFeedUrl: string | null; isActive: boolean; lastFullImportAt: string | null; lastOfferSyncAt: string | null; }; function formatDate(value: string | null) { if (!value) return "—"; const d = new Date(value); if (Number.isNaN(d.getTime())) return value; return d.toLocaleString(); } export default function MerchantsAdminPage() { const [merchants, setMerchants] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [savingId, setSavingId] = useState(null); const [importingId, setImportingId] = useState(null); const [offerSyncId, setOfferSyncId] = useState(null); const [banner, setBanner] = useState(null); // --- load merchants --- useEffect(() => { const controller = new AbortController(); async function loadMerchants() { try { setLoading(true); setError(null); const url = `${API_BASE_URL}/api/admin/merchants`; console.log("Loading merchants from:", url); const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" }, signal: controller.signal, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`HTTP ${res.status} ${res.statusText} – ${text}`); } const data: MerchantAdminDto[] = await res.json(); setMerchants(data); } catch (err: any) { if (err?.name === "AbortError") return; console.error("Error loading merchants", err); setError(err?.message ?? "Failed to load merchants"); } finally { setLoading(false); } } loadMerchants(); return () => controller.abort(); }, []); // --- local field editing --- const updateMerchantField = ( id: number, field: K, value: MerchantAdminDto[K] ) => { setMerchants((prev) => prev.map((m) => (m.id === id ? { ...m, [field]: value } : m)) ); }; // --- save merchant (name/feed URLs/isActive) --- const handleSave = async (id: number) => { const merchant = merchants.find((m) => m.id === id); if (!merchant) return; try { setSavingId(id); setError(null); setBanner(null); const res = await fetch(`${API_BASE_URL}/api/admin/merchants/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: merchant.name, avantlinkMid: merchant.avantlinkMid, feedUrl: merchant.feedUrl, offerFeedUrl: merchant.offerFeedUrl, isActive: merchant.isActive, }), }); if (!res.ok) { const text = await res.text().catch(() => ""); console.error("Merchant save failed", res.status, text); throw new Error(`Save failed (${res.status})${text ? `: ${text}` : ""}`); } const updated: MerchantAdminDto = await res.json(); setMerchants((prev) => prev.map((m) => (m.id === id ? updated : m))); setBanner("Merchant saved successfully."); } catch (e: any) { console.error("Error saving merchant", e); setError(e?.message ?? "Failed to save merchant"); } finally { setSavingId(null); setTimeout(() => setBanner(null), 3000); } }; // --- run full import --- const handleRunFullImport = async (id: number) => { try { setImportingId(id); setError(null); setBanner(null); const res = await fetch(`${API_BASE_URL}/api/admin/imports/${id}`, { method: "POST", }); if (!res.ok) { const text = await res.text().catch(() => ""); console.error("Full import failed", res.status, text); throw new Error(`Import failed (${res.status})${text ? `: ${text}` : ""}`); } setBanner("Full import started successfully."); } catch (e: any) { console.error("Error starting full import", e); setError(e?.message ?? "Failed to start full import"); } finally { setImportingId(null); setTimeout(() => setBanner(null), 3000); } }; // --- run offer sync --- const handleRunOfferSync = async (id: number) => { try { setOfferSyncId(id); setError(null); setBanner(null); const res = await fetch( `${API_BASE_URL}/api/admin/imports/${id}/offers-only`, { method: "POST" } ); if (!res.ok) { const text = await res.text().catch(() => ""); console.error("Offer sync failed", res.status, text); throw new Error( `Offer sync failed (${res.status})${text ? `: ${text}` : ""}` ); } setBanner("Offer sync started successfully."); } catch (e: any) { console.error("Error starting offer sync", e); setError(e?.message ?? "Failed to start offer sync"); } finally { setOfferSyncId(null); setTimeout(() => setBanner(null), 3000); } }; return (
{/* Header */}

Shadow Standard

Merchant Feeds Admin

Manage your AvantLink merchants, product feed URLs, and offer sync settings. Use this to onboard new merchants and keep feeds fresh without touching SQL.

{/* Status banners */} {banner && (
{banner}
)} {error && (
{error}
)} {/* Table */}
{loading ? (

Loading merchants…

) : merchants.length === 0 ? (

No merchants found. Seed some merchants in the backend.

) : (
{merchants.map((m) => ( ))}
Merchant AvantLink MID Feed URL Offer Feed URL Active Last Full Import Last Offer Sync Actions
updateMerchantField(m.id, "name", e.target.value) } className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" /> updateMerchantField(m.id, "avantlinkMid", e.target.value) } className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" /> updateMerchantField(m.id, "feedUrl", e.target.value) } className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs md:text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" /> updateMerchantField(m.id, "offerFeedUrl", e.target.value || null) } className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs md:text-sm text-zinc-50 focus:border-amber-400/70 focus:outline-none focus:ring-0" /> {formatDate(m.lastFullImportAt)} {formatDate(m.lastOfferSyncAt)}
)}
); }