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 [offerSyncId, setOfferSyncId] = useState<number | null>(null);
|
||||||
const [banner, setBanner] = useState<string | 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 ---
|
// --- load merchants ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
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 (
|
return (
|
||||||
<main className="min-h-screen bg-black text-zinc-50">
|
<main className="min-h-screen bg-black text-zinc-50">
|
||||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||||
@@ -211,6 +272,152 @@ export default function MerchantsAdminPage() {
|
|||||||
</div>
|
</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 */}
|
{/* Table */}
|
||||||
<section className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-3 md:p-4">
|
<section className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-3 md:p-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ export default function PartsBrowseClient(props: {
|
|||||||
end: Math.min(currentPage * PAGE_SIZE, serverTotalElements),
|
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.";
|
const headingSubtitle = props.subtitle ?? "Browse available parts.";
|
||||||
|
|
||||||
// ----------------------------
|
// ----------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user