Add VendorsCard to admin dashboard
Introduces a new VendorsCard component for managing vendor accounts in the admin dashboard. The card displays vendor details, allows password reset token generation, and handles loading and error states.
This commit is contained in:
@@ -5,6 +5,7 @@ import BanUserCard from "@/components/admin/BanUserCard";
|
|||||||
import RecentOrdersCard from "@/components/admin/RecentOrdersCard";
|
import RecentOrdersCard from "@/components/admin/RecentOrdersCard";
|
||||||
import SystemStatusCard from "@/components/admin/SystemStatusCard";
|
import SystemStatusCard from "@/components/admin/SystemStatusCard";
|
||||||
import InvitationsListCard from "@/components/admin/InvitationsListCard";
|
import InvitationsListCard from "@/components/admin/InvitationsListCard";
|
||||||
|
import VendorsCard from "@/components/admin/VendorsCard";
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
return (
|
return (
|
||||||
@@ -16,7 +17,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 items-stretch">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 items-stretch">
|
||||||
<SystemStatusCard />
|
<SystemStatusCard />
|
||||||
|
<VendorsCard />
|
||||||
<InviteVendorCard />
|
<InviteVendorCard />
|
||||||
<BanUserCard />
|
<BanUserCard />
|
||||||
<RecentOrdersCard />
|
<RecentOrdersCard />
|
||||||
|
|||||||
97
components/admin/VendorsCard.tsx
Normal file
97
components/admin/VendorsCard.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { fetchClient } from "@/lib/api-client";
|
||||||
|
|
||||||
|
interface Vendor {
|
||||||
|
_id: string;
|
||||||
|
username: string;
|
||||||
|
email?: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
lastLogin?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VendorsCard() {
|
||||||
|
const [vendors, setVendors] = useState<Vendor[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [resetTokens, setResetTokens] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchClient<Vendor[]>("/admin/vendors");
|
||||||
|
if (mounted) setVendors(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (mounted) setError(e?.message || "Failed to load vendors");
|
||||||
|
} finally {
|
||||||
|
if (mounted) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { mounted = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function generateResetToken(vendorId: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetchClient<{ token: string; expiresAt: string }>("/admin/password-reset-token", {
|
||||||
|
method: "POST",
|
||||||
|
body: { vendorId }
|
||||||
|
});
|
||||||
|
setResetTokens(prev => ({ ...prev, [vendorId]: res.token }));
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e?.message || "Failed to generate reset token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||||||
|
<h2 className="font-medium">Vendors</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Manage vendor accounts and access</p>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">Loading...</p>
|
||||||
|
) : error ? (
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">{error}</p>
|
||||||
|
) : vendors.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground mt-3">No vendors found</p>
|
||||||
|
) : (
|
||||||
|
<div className="mt-3 space-y-2 max-h-64 overflow-y-auto">
|
||||||
|
{vendors.map((vendor) => (
|
||||||
|
<div key={vendor._id} className="rounded border border-border/50 p-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="font-medium">{vendor.username}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{vendor.email && `${vendor.email} · `}
|
||||||
|
Created: {new Date(vendor.createdAt).toLocaleDateString()}
|
||||||
|
{vendor.lastLogin && ` · Last login: ${new Date(vendor.lastLogin).toLocaleDateString()}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{vendor.isAdmin && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400">
|
||||||
|
Admin
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => generateResetToken(vendor._id)}
|
||||||
|
className="text-xs px-2 py-1 rounded border border-border hover:bg-muted/40"
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{resetTokens[vendor._id] && (
|
||||||
|
<div className="mt-2 p-2 rounded bg-muted/40 text-xs">
|
||||||
|
<div className="font-mono break-all">{resetTokens[vendor._id]}</div>
|
||||||
|
<div className="text-muted-foreground mt-1">Copy this token to share with the vendor</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user