Add pagination to admin user, vendor, and ban lists
Introduces pagination controls and server-side paginated fetching for blocked users, users, and vendors in the admin dashboard. Improves error handling in server API responses and validates order ID in OrderDetailsModal. Updates git-info.json with latest commit metadata.
This commit is contained in:
@@ -33,6 +33,15 @@ export default function AdminBanPage() {
|
||||
const [blockedUsers, setBlockedUsers] = useState<BlockedUser[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [banDialogOpen, setBanDialogOpen] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pagination, setPagination] = useState<{
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNextPage: boolean;
|
||||
hasPrevPage: boolean;
|
||||
} | null>(null);
|
||||
const [banData, setBanData] = useState({
|
||||
telegramUserId: "",
|
||||
reason: "",
|
||||
@@ -41,13 +50,25 @@ export default function AdminBanPage() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchBlockedUsers();
|
||||
}, []);
|
||||
}, [page]);
|
||||
|
||||
const fetchBlockedUsers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchClient<BlockedUser[]>("/admin/blocked-users");
|
||||
setBlockedUsers(data);
|
||||
const data = await fetchClient<{
|
||||
success: boolean;
|
||||
blockedUsers: BlockedUser[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNextPage: boolean;
|
||||
hasPrevPage: boolean;
|
||||
};
|
||||
}>(`/admin/blocked-users?page=${page}&limit=25`);
|
||||
setBlockedUsers(data.blockedUsers);
|
||||
setPagination(data.pagination);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch blocked users:", error);
|
||||
toast({
|
||||
@@ -395,6 +416,31 @@ export default function AdminBanPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between px-6 pb-6">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing page {pagination.page} of {pagination.totalPages} ({pagination.total} total)
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={!pagination.hasPrevPage}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
disabled={!pagination.hasNextPage}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,19 @@ interface TelegramUser {
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
interface PaginationResponse {
|
||||
success: boolean;
|
||||
users: TelegramUser[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNextPage: boolean;
|
||||
hasPrevPage: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('en-GB', {
|
||||
style: 'currency',
|
||||
@@ -37,16 +50,19 @@ export default function AdminUsersPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [users, setUsers] = useState<TelegramUser[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
}, [page]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchClient<TelegramUser[]>("/admin/users");
|
||||
setUsers(data);
|
||||
const data = await fetchClient<PaginationResponse>(`/admin/users?page=${page}&limit=25`);
|
||||
setUsers(data.users);
|
||||
setPagination(data.pagination);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to fetch users:", error);
|
||||
toast({
|
||||
@@ -300,6 +316,31 @@ export default function AdminUsersPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing page {pagination.page} of {pagination.totalPages} ({pagination.total} total users)
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={!pagination.hasPrevPage}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
disabled={!pagination.hasNextPage}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -157,10 +157,21 @@ export default function OrderDetailsModal({ orderId, open, onOpenChange }: Order
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Validate orderId before making request
|
||||
if (!orderId || orderId === 'undefined' || orderId === 'null') {
|
||||
throw new Error('Order ID is required');
|
||||
}
|
||||
|
||||
// Ensure orderId is a valid number or string
|
||||
const orderIdStr = String(orderId).trim();
|
||||
if (!orderIdStr || orderIdStr === 'undefined' || orderIdStr === 'null') {
|
||||
throw new Error('Invalid order ID');
|
||||
}
|
||||
|
||||
// Fetch full order details from admin endpoint
|
||||
// Use /admin/orders/:orderId (fetchClient will add /api prefix and backend URL)
|
||||
console.log(`Fetching order details for order #${orderId}`);
|
||||
const orderData = await fetchClient<OrderDetails>(`/admin/orders/${orderId}`);
|
||||
console.log(`Fetching order details for order #${orderIdStr}`);
|
||||
const orderData = await fetchClient<OrderDetails>(`/admin/orders/${orderIdStr}`);
|
||||
|
||||
console.log("Order data received:", orderData);
|
||||
|
||||
|
||||
@@ -10,18 +10,37 @@ interface Vendor {
|
||||
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 {
|
||||
const data = await fetchClient<Vendor[]>("/admin/vendors");
|
||||
if (mounted) setVendors(data);
|
||||
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 {
|
||||
@@ -29,7 +48,7 @@ export default function VendorsCard() {
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
}, [page]);
|
||||
|
||||
async function generateResetToken(vendorId: string) {
|
||||
try {
|
||||
@@ -55,6 +74,7 @@ export default function VendorsCard() {
|
||||
) : 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">
|
||||
@@ -89,6 +109,30 @@ export default function VendorsCard() {
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -86,8 +86,26 @@ export async function fetchServer<T = unknown>(
|
||||
|
||||
// Handle other errors
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`;
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await res.json();
|
||||
} catch {
|
||||
errorData = {};
|
||||
}
|
||||
|
||||
// Handle new error format: { success: false, error: { message: "...", code: "..." } }
|
||||
// or old format: { error: "...", message: "..." }
|
||||
let errorMessage: string;
|
||||
if (errorData.error?.message) {
|
||||
errorMessage = errorData.error.message;
|
||||
} else if (typeof errorData.error === 'string') {
|
||||
errorMessage = errorData.error;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
} else {
|
||||
errorMessage = `Request failed: ${res.status} ${res.statusText}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
@@ -96,10 +114,24 @@ export async function fetchServer<T = unknown>(
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
try {
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, throw a proper error
|
||||
throw new Error(`Failed to parse response from ${endpoint}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Server request to ${endpoint} failed:`, error);
|
||||
// Ensure we always throw an Error instance, not an object
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
} else if (typeof error === 'string') {
|
||||
throw new Error(error);
|
||||
} else {
|
||||
const errorStr = error && typeof error === 'object' ? JSON.stringify(error) : String(error);
|
||||
throw new Error(`Request failed: ${errorStr}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"commitHash": "96638f9",
|
||||
"buildTime": "2025-12-31T05:14:19.565Z"
|
||||
"commitHash": "0062aa2",
|
||||
"buildTime": "2025-12-31T05:39:04.712Z"
|
||||
}
|
||||
Reference in New Issue
Block a user