a metric shit ton. fixed a lot of part_role issues. removed almost all hardcoded categories and used db roles.

This commit is contained in:
2025-12-15 20:59:19 -05:00
parent 0f10ff4e09
commit 607939c468
16 changed files with 1538 additions and 1065 deletions
+51 -75
View File
@@ -34,52 +34,50 @@ export default function MerchantsAdminPage() {
// --- load merchants ---
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
async function loadMerchants() {
try {
console.log("Loading merchants from:", `${API_BASE_URL}/admin/merchants`);
const res = await fetch(`${API_BASE_URL}/admin/merchants`, {
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",
},
headers: { Accept: "application/json" },
signal: controller.signal,
});
console.log("Merchants response status:", res.status);
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);
setError(null);
} catch (err: any) {
if (err?.name === "AbortError") return;
console.error("Error loading merchants", err);
setError(err.message ?? "Failed to load merchants");
setError(err?.message ?? "Failed to load merchants");
} finally {
setLoading(false);
}
}
loadMerchants();
return () => {
cancelled = true;
};
loadMerchants();
return () => controller.abort();
}, []);
// --- local field editing ---
const updateMerchantField = <K extends keyof MerchantAdminDto>(
id: number,
field: K,
value: MerchantAdminDto[K],
value: MerchantAdminDto[K]
) => {
setMerchants((prev) =>
prev.map((m) => (m.id === id ? { ...m, [field]: value } : m)),
prev.map((m) => (m.id === id ? { ...m, [field]: value } : m))
);
};
@@ -93,7 +91,7 @@ export default function MerchantsAdminPage() {
setError(null);
setBanner(null);
const res = await fetch(`${API_BASE_URL}/admin/merchants/${id}`, {
const res = await fetch(`${API_BASE_URL}/api/admin/merchants/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -108,20 +106,16 @@ export default function MerchantsAdminPage() {
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}` : ""}`,
);
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)),
);
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");
setError(e?.message ?? "Failed to save merchant");
} finally {
setSavingId(null);
setTimeout(() => setBanner(null), 3000);
@@ -135,29 +129,27 @@ export default function MerchantsAdminPage() {
setError(null);
setBanner(null);
const res = await fetch(`${API_BASE_URL}/admin/imports/${id}`, {
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}` : ""}`,
);
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");
setError(e?.message ?? "Failed to start full import");
} finally {
setImportingId(null);
setTimeout(() => setBanner(null), 3000);
}
};
// --- run offer sync (matches /{merchantId}/offers-only) ---
// --- run offer sync ---
const handleRunOfferSync = async (id: number) => {
try {
setOfferSyncId(id);
@@ -165,24 +157,22 @@ export default function MerchantsAdminPage() {
setBanner(null);
const res = await fetch(
`${API_BASE_URL}/admin/imports/${id}/offers-only`,
{
method: "POST",
},
`${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}` : ""}`,
`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");
setError(e?.message ?? "Failed to start offer sync");
} finally {
setOfferSyncId(null);
setTimeout(() => setBanner(null), 3000);
@@ -202,9 +192,9 @@ export default function MerchantsAdminPage() {
Merchant Feeds <span className="text-amber-300">Admin</span>
</h1>
<p className="mt-2 text-sm text-zinc-400 max-w-2xl">
Manage your AvantLink merchants, product feed URLs, and offer
sync settings. Use this to onboard new merchants and keep feeds
fresh without touching SQL.
Manage your AvantLink merchants, product feed URLs, and offer sync
settings. Use this to onboard new merchants and keep feeds fresh
without touching SQL.
</p>
</div>
</header>
@@ -244,6 +234,7 @@ export default function MerchantsAdminPage() {
<th className="py-2 pl-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800">
{merchants.map((m) => (
<tr key={m.id} className="align-top">
@@ -257,71 +248,62 @@ export default function MerchantsAdminPage() {
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"
/>
</td>
<td className="py-2 px-3">
<input
type="text"
value={m.avantlinkMid}
onChange={(e) =>
updateMerchantField(
m.id,
"avantlinkMid",
e.target.value,
)
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"
/>
</td>
<td className="py-2 px-3">
<input
type="text"
value={m.feedUrl ?? ""}
onChange={(e) =>
updateMerchantField(
m.id,
"feedUrl",
e.target.value,
)
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"
/>
</td>
<td className="py-2 px-3">
<input
type="text"
value={m.offerFeedUrl ?? ""}
onChange={(e) =>
updateMerchantField(
m.id,
"offerFeedUrl",
e.target.value || null,
)
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"
/>
</td>
<td className="py-2 px-3">
<label className="inline-flex items-center gap-2 text-xs text-zinc-300">
<input
type="checkbox"
checked={m.isActive}
onChange={(e) =>
updateMerchantField(
m.id,
"isActive",
e.target.checked,
)
updateMerchantField(m.id, "isActive", e.target.checked)
}
className="h-4 w-4 rounded border-zinc-600 bg-zinc-900 text-amber-400 focus:ring-amber-400"
/>
Active
</label>
</td>
<td className="py-2 px-3 text-xs text-zinc-400 whitespace-nowrap">
{formatDate(m.lastFullImportAt)}
</td>
<td className="py-2 px-3 text-xs text-zinc-400 whitespace-nowrap">
{formatDate(m.lastOfferSyncAt)}
</td>
<td className="py-2 pl-3">
<div className="flex flex-col items-end gap-1">
<button
@@ -336,33 +318,27 @@ export default function MerchantsAdminPage() {
>
{savingId === m.id ? "Saving…" : "Save"}
</button>
<button
type="button"
onClick={() => handleRunFullImport(m.id)}
disabled={importingId === m.id}
className={`rounded-md px-3 py-1.5 text-xs font-medium border border-zinc-700 bg-zinc-900 text-zinc-200 hover:bg-zinc-800 transition-colors ${
importingId === m.id
? "opacity-60 cursor-wait"
: ""
importingId === m.id ? "opacity-60 cursor-wait" : ""
}`}
>
{importingId === m.id
? "Importing…"
: "Run Full Import"}
{importingId === m.id ? "Importing…" : "Run Full Import"}
</button>
<button
type="button"
onClick={() => handleRunOfferSync(m.id)}
disabled={offerSyncId === m.id}
className={`rounded-md px-3 py-1.5 text-xs font-medium border border-zinc-700 bg-zinc-900 text-zinc-200 hover:bg-zinc-800 transition-colors ${
offerSyncId === m.id
? "opacity-60 cursor-wait"
: ""
offerSyncId === m.id ? "opacity-60 cursor-wait" : ""
}`}
>
{offerSyncId === m.id
? "Syncing Offers…"
: "Run Offer Sync"}
{offerSyncId === m.id ? "Syncing Offers…" : "Run Offer Sync"}
</button>
</div>
</td>