Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { fetchClient } from "@/lib/api/api-client";
|
|
|
|
interface Vendor {
|
|
_id: string;
|
|
username: string;
|
|
isAdmin: boolean;
|
|
createdAt: string;
|
|
lastLogin?: string;
|
|
}
|
|
|
|
interface PaginationResponse {
|
|
success: boolean;
|
|
vendors: Vendor[];
|
|
pagination: {
|
|
page: number;
|
|
limit: number;
|
|
total: number;
|
|
totalPages: number;
|
|
hasNextPage: boolean;
|
|
hasPrevPage: boolean;
|
|
};
|
|
}
|
|
|
|
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>>({});
|
|
const [page, setPage] = useState(1);
|
|
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await fetchClient<PaginationResponse>(`/admin/vendors?page=${page}&limit=10`);
|
|
if (mounted) {
|
|
setVendors(data.vendors);
|
|
setPagination(data.pagination);
|
|
}
|
|
} catch (e: any) {
|
|
if (mounted) setError(e?.message || "Failed to load vendors");
|
|
} finally {
|
|
if (mounted) setLoading(false);
|
|
}
|
|
})();
|
|
return () => { mounted = false; };
|
|
}, [page]);
|
|
|
|
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">
|
|
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>
|
|
{pagination && pagination.totalPages > 1 && (
|
|
<div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
|
|
<span>
|
|
Page {pagination.page} of {pagination.totalPages} ({pagination.total} total)
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
disabled={!pagination.hasPrevPage}
|
|
className="px-2 py-1 rounded border border-border hover:bg-muted/40 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Previous
|
|
</button>
|
|
<button
|
|
onClick={() => setPage(p => p + 1)}
|
|
disabled={!pagination.hasNextPage}
|
|
className="px-2 py-1 rounded border border-border hover:bg-muted/40 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|