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:
NotII
2025-10-16 00:29:32 +01:00
parent 66c95fdffe
commit 2800f03335
2 changed files with 99 additions and 1 deletions

View File

@@ -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 />

View 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>
);
}