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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user