Add new merchant creation form and functionality in MerchantsAdminPage
CI / test (push) Successful in 5s
CI / test (push) Successful in 5s
This commit is contained in:
@@ -32,6 +32,17 @@ export default function MerchantsAdminPage() {
|
||||
const [offerSyncId, setOfferSyncId] = useState<number | null>(null);
|
||||
const [banner, setBanner] = useState<string | null>(null);
|
||||
|
||||
// --- new merchant form state ---
|
||||
const [showNewMerchantForm, setShowNewMerchantForm] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newMerchant, setNewMerchant] = useState({
|
||||
name: "",
|
||||
avantlinkMid: "",
|
||||
feedUrl: "",
|
||||
offerFeedUrl: "",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// --- load merchants ---
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
@@ -179,6 +190,56 @@ export default function MerchantsAdminPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// --- create new merchant ---
|
||||
const handleCreateMerchant = async () => {
|
||||
try {
|
||||
setCreating(true);
|
||||
setError(null);
|
||||
setBanner(null);
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/merchants`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: newMerchant.name,
|
||||
avantlinkMid: newMerchant.avantlinkMid,
|
||||
feedUrl: newMerchant.feedUrl,
|
||||
offerFeedUrl: newMerchant.offerFeedUrl || null,
|
||||
isActive: newMerchant.isActive,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
console.error("Merchant creation failed", res.status, text);
|
||||
throw new Error(
|
||||
`Creation failed (${res.status})${text ? `: ${text}` : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
const created: MerchantAdminDto = await res.json();
|
||||
setMerchants((prev) => [...prev, created]);
|
||||
|
||||
// Reset form
|
||||
setNewMerchant({
|
||||
name: "",
|
||||
avantlinkMid: "",
|
||||
feedUrl: "",
|
||||
offerFeedUrl: "",
|
||||
isActive: true,
|
||||
});
|
||||
setShowNewMerchantForm(false);
|
||||
|
||||
setBanner("Merchant created successfully.");
|
||||
} catch (e: any) {
|
||||
console.error("Error creating merchant", e);
|
||||
setError(e?.message ?? "Failed to create merchant");
|
||||
} finally {
|
||||
setCreating(false);
|
||||
setTimeout(() => setBanner(null), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-zinc-50">
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||
@@ -211,6 +272,152 @@ export default function MerchantsAdminPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add New Merchant Section */}
|
||||
<section className="mb-6 rounded-lg border border-zinc-800 bg-zinc-950/70 p-4">
|
||||
{!showNewMerchantForm ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewMerchantForm(true)}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-amber-400 px-4 py-2 text-sm font-semibold text-black hover:bg-amber-300 transition-colors"
|
||||
>
|
||||
<span className="text-lg">+</span>
|
||||
Add New Merchant
|
||||
</button>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-zinc-100">
|
||||
Add New Merchant
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowNewMerchantForm(false);
|
||||
setNewMerchant({
|
||||
name: "",
|
||||
avantlinkMid: "",
|
||||
feedUrl: "",
|
||||
offerFeedUrl: "",
|
||||
isActive: true,
|
||||
});
|
||||
}}
|
||||
className="text-zinc-400 hover:text-zinc-200 text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-1">
|
||||
Merchant Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newMerchant.name}
|
||||
onChange={(e) =>
|
||||
setNewMerchant((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
placeholder="e.g., Brownells"
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-1">
|
||||
AvantLink MID *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newMerchant.avantlinkMid}
|
||||
onChange={(e) =>
|
||||
setNewMerchant((prev) => ({
|
||||
...prev,
|
||||
avantlinkMid: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="e.g., 12345"
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-1">
|
||||
Feed URL *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newMerchant.feedUrl}
|
||||
onChange={(e) =>
|
||||
setNewMerchant((prev) => ({ ...prev, feedUrl: e.target.value }))
|
||||
}
|
||||
placeholder="https://..."
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-1">
|
||||
Offer Feed URL (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newMerchant.offerFeedUrl}
|
||||
onChange={(e) =>
|
||||
setNewMerchant((prev) => ({
|
||||
...prev,
|
||||
offerFeedUrl: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="https://..."
|
||||
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-50 placeholder:text-zinc-600 focus:border-amber-400/70 focus:outline-none focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="inline-flex items-center gap-2 text-sm text-zinc-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newMerchant.isActive}
|
||||
onChange={(e) =>
|
||||
setNewMerchant((prev) => ({
|
||||
...prev,
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateMerchant}
|
||||
disabled={
|
||||
creating ||
|
||||
!newMerchant.name ||
|
||||
!newMerchant.avantlinkMid ||
|
||||
!newMerchant.feedUrl
|
||||
}
|
||||
className={`rounded-md px-4 py-2 text-sm font-semibold transition-colors ${
|
||||
creating ||
|
||||
!newMerchant.name ||
|
||||
!newMerchant.avantlinkMid ||
|
||||
!newMerchant.feedUrl
|
||||
? "bg-amber-400/20 text-amber-200 cursor-not-allowed"
|
||||
: "bg-amber-400 text-black hover:bg-amber-300"
|
||||
}`}
|
||||
>
|
||||
{creating ? "Creating…" : "Create Merchant"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Table */}
|
||||
<section className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-3 md:p-4">
|
||||
{loading ? (
|
||||
|
||||
@@ -303,7 +303,7 @@ export default function PartsBrowseClient(props: {
|
||||
end: Math.min(currentPage * PAGE_SIZE, serverTotalElements),
|
||||
};
|
||||
|
||||
const headingTitle = props.title ?? `${partRole.replaceAll("-", " ")} parts`;
|
||||
const headingTitle = props.title ?? `${partRole.replaceAll("-", " ")}s`;
|
||||
const headingSubtitle = props.subtitle ?? "Browse available parts.";
|
||||
|
||||
// ----------------------------
|
||||
|
||||
Reference in New Issue
Block a user