"use client"; import { useState, useEffect } from "react"; import { Table, Input, message, Modal } from "antd"; import type { ColumnsType } from "antd/es/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { useTranslation } from "react-i18next"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Trash2, Search, X, Unlock, Lock } from "lucide-react"; import { EditableCell } from "@/components/editable-cell"; import { motion, AnimatePresence } from "framer-motion"; import { createPortal } from "react-dom"; import { toast, Toaster } from "sonner"; interface User { id: string; email: string; name: string; role: string; balance: number; deleted: boolean; } interface TFunction { (key: string): string; (key: string, options: { name: string }): string; } const TABLE_STYLES = ` [&_.ant-table]:!border-b-0 [&_.ant-table-container]:!rounded-xl [&_.ant-table-container]:!border-hidden [&_.ant-table-cell]:!border-border/40 [&_.ant-table-thead_.ant-table-cell]:!bg-muted/30 [&_.ant-table-thead_.ant-table-cell]:!text-muted-foreground [&_.ant-table-thead_.ant-table-cell]:!font-medium [&_.ant-table-thead_.ant-table-cell]:!text-sm [&_.ant-table-thead]:!border-b [&_.ant-table-thead]:border-border/40 [&_.ant-table-row]:!transition-colors [&_.ant-table-row:hover>*]:!bg-muted/60 [&_.ant-table-tbody_.ant-table-row]:!cursor-pointer [&_.ant-table-tbody_.ant-table-cell]:!py-4 [&_.ant-table-row:last-child>td]:!border-b-0 [&_.ant-table-cell:first-child]:!pl-6 [&_.ant-table-cell:last-child]:!pr-6 [&_.ant-pagination]:!px-6 [&_.ant-pagination]:!py-4 [&_.ant-pagination]:!border-t [&_.ant-pagination]:border-border/40 [&_.ant-pagination-item]:!rounded-lg [&_.ant-pagination-item]:!border-border/40 [&_.ant-pagination-item-active]:!bg-primary/10 [&_.ant-pagination-item-active]:!border-primary/30 [&_.ant-pagination-item-active>a]:!text-primary [&_.ant-pagination-prev_.ant-pagination-item-link]:!rounded-lg [&_.ant-pagination-next_.ant-pagination-item-link]:!rounded-lg [&_.ant-pagination-prev_.ant-pagination-item-link]:!border-border/40 [&_.ant-pagination-next_.ant-pagination-item-link]:!border-border/40 `; const formatBalance = (balance: number | string) => { const num = typeof balance === "number" ? balance : Number(balance); return isFinite(num) ? num.toFixed(4) : "0.0000"; }; const UserDetailsModal = ({ user, onClose, t, }: { user: User | null; onClose: () => void; t: TFunction; }) => { if (!user) return null; return createPortal( e.stopPropagation()} >
{user.name.charAt(0).toUpperCase()}

{user.name}

{user.role}
{t("users.email")}
{user.email}
{t("users.id")}
{user.id}
{t("users.balance")}
{formatBalance(user.balance)}
, document.getElementById("modal-root") || document.body ); }; const BlockConfirmModal = ({ user, onClose, onConfirm, t, }: { user: User | null; onClose: () => void; onConfirm: () => void; t: TFunction; }) => { if (!user) return null; return createPortal( e.stopPropagation()} >
{user.deleted ? ( ) : ( )}

{user.deleted ? t("users.blacklist.unblockConfirm.title") : t("users.blacklist.blockConfirm.title")}

{user.deleted ? t("users.blacklist.unblockConfirm.description", { name: user.name, }) : t("users.blacklist.blockConfirm.description", { name: user.name, })}

{t("common.cancel")} {user.deleted ? t("users.blacklist.unblock") : t("users.blacklist.block")}
, document.getElementById("modal-root") || document.body ); }; const LoadingState = ({ t }: { t: TFunction }) => (

{t("users.loading")}

); export default function UsersPage() { const { t } = useTranslation("common"); const [users, setUsers] = useState([]); const [blacklistUsers, setBlacklistUsers] = useState([]); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [editingKey, setEditingKey] = useState(""); const [selectedUser, setSelectedUser] = useState(null); const [userToDelete, setUserToDelete] = useState(null); const [sortInfo, setSortInfo] = useState<{ field: string | null; order: "ascend" | "descend" | null; }>({ field: null, order: null, }); const [searchText, setSearchText] = useState(""); const [showBlacklist, setShowBlacklist] = useState(false); const [blacklistCurrentPage, setBlacklistCurrentPage] = useState(1); const [blacklistTotal, setBlacklistTotal] = useState(0); const fetchUsers = async (page: number, isBlacklist: boolean = false) => { setLoading(true); try { let url = `/api/v1/users?page=${page}&deleted=${isBlacklist}`; if (sortInfo.field && sortInfo.order) { url += `&sortField=${sortInfo.field}&sortOrder=${sortInfo.order}`; } if (searchText) { url += `&search=${encodeURIComponent(searchText)}`; } const token = localStorage.getItem("access_token"); const res = await fetch(url, { headers: { Authorization: `Bearer ${token}`, }, }); const data = await res.json(); if (!res.ok) throw new Error(data.error); if (isBlacklist) { setBlacklistUsers(data.users); setBlacklistTotal(data.total); } else { setUsers(data.users); setTotal(data.total); } } catch (err) { console.error(err); message.error(t("users.message.fetchError")); } finally { setLoading(false); } }; const fetchBlacklistTotal = async () => { try { const token = localStorage.getItem("access_token"); const res = await fetch(`/api/v1/users?page=1&deleted=true&pageSize=1`, { headers: { Authorization: `Bearer ${token}`, }, }); const data = await res.json(); if (!res.ok) throw new Error(data.error); setBlacklistTotal(data.total); } catch (err) { console.error(err); } }; useEffect(() => { fetchUsers(currentPage, false); fetchBlacklistTotal(); }, [currentPage, sortInfo, searchText]); useEffect(() => { if (showBlacklist) { fetchUsers(blacklistCurrentPage, true); } }, [blacklistCurrentPage, showBlacklist, sortInfo, searchText]); const handleUpdateBalance = async (userId: string, newBalance: number) => { try { console.log(`Updating balance for user ${userId} to ${newBalance}`); const token = localStorage.getItem("access_token"); if (!token) { throw new Error(t("auth.unauthorized")); } const res = await fetch(`/api/v1/users/${userId}/balance`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ balance: newBalance }), }); const data = await res.json(); console.log("Update balance response:", data); if (!res.ok) { throw new Error(data.error || t("users.message.updateBalance.error")); } setUsers( users.map((user) => user.id === userId ? { ...user, balance: newBalance } : user ) ); toast.success(t("users.message.updateBalance.success")); setEditingKey(""); fetchUsers(currentPage, false); } catch (err) { console.error("Failed to update balance:", err); toast.error( err instanceof Error ? err.message : t("users.message.updateBalance.error") ); } }; const handleDeleteUser = async () => { if (!userToDelete) return; try { const token = localStorage.getItem("access_token"); const res = await fetch(`/api/v1/users/${userToDelete.id}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ deleted: !userToDelete.deleted, }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.message); } if (!userToDelete.deleted) { const newTotal = total - 1; const maxPage = Math.ceil(newTotal / 20); if (currentPage > maxPage && maxPage > 0) { setCurrentPage(maxPage); } else { fetchUsers(currentPage, false); } setBlacklistTotal((prev) => prev + 1); } else { const newBlacklistTotal = blacklistTotal - 1; const maxBlacklistPage = Math.ceil(newBlacklistTotal / 20); if (blacklistCurrentPage > maxBlacklistPage && maxBlacklistPage > 0) { setBlacklistCurrentPage(maxBlacklistPage); } else { fetchUsers(blacklistCurrentPage, true); } setBlacklistTotal((prev) => prev - 1); } if (userToDelete.deleted) { fetchUsers(currentPage, false); } else if (showBlacklist) { fetchUsers(blacklistCurrentPage, true); } toast.success( userToDelete.deleted ? t("users.message.unblockSuccess") : t("users.message.blockSuccess") ); } catch (err) { toast.error( userToDelete.deleted ? t("users.message.unblockError") : t("users.message.blockError") ); } finally { setUserToDelete(null); } }; const UserCard = ({ record }: { record: User }) => { return (
setSelectedUser(record)} >
{record.name.charAt(0).toUpperCase()}

{record.name}

{record.role}

{record.email}

{t("users.balance")}
setEditingKey(record.id)} onSubmit={(value) => handleUpdateBalance(record.id, value)} onCancel={() => setEditingKey("")} t={t} validateValue={(value) => ({ isValid: isFinite(value), errorMessage: t("error.invalidNumber"), maxValue: 999999.9999, })} />
); }; const getColumns = (isBlacklist: boolean = false): ColumnsType => { const baseColumns: ColumnsType = [ { title: t("users.userInfo"), key: "userInfo", width: "65%", render: (_, record) => (
setSelectedUser(record)} >
{record.name.charAt(0).toUpperCase()}
{record.name} {record.role}
{record.email}
), }, { title: t("users.balance"), dataIndex: "balance", key: "balance", width: "35%", align: "left", sorter: { compare: (a, b) => a.balance - b.balance, multiple: 1, }, render: (balance: number, record) => { const isEditing = record.id === editingKey; return (
setEditingKey(record.id)} onSubmit={(value) => handleUpdateBalance(record.id, value)} onCancel={() => setEditingKey("")} t={t} validateValue={(value) => ({ isValid: isFinite(value), errorMessage: t("error.invalidNumber"), maxValue: 999999.9999, })} />
); }, }, { title: t("users.actions"), key: "actions", width: "48px", align: "center", render: (_, record) => ( ), }, ]; return baseColumns; }; const SearchBar = () => { const [isFocused, setIsFocused] = useState(false); const [searchValue, setSearchValue] = useState(searchText); const handleSearch = () => { setSearchText(searchValue); }; const handleClear = () => { setSearchValue(""); setTimeout(() => { setSearchText(""); }, 0); }; return (
setSearchValue(e.target.value)} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} onKeyDown={(e) => { if (e.key === "Enter") { handleSearch(); } }} className=" w-full pl-12 pr-24 py-3 h-12 leading-normal bg-transparent border-0 ring-0 focus:ring-0 placeholder:text-muted-foreground/50 text-base [&::-webkit-search-cancel-button]:hidden " allowClear={{ clearIcon: searchValue ? ( ) : null, }} />
); }; const EmptyState = ({ searchText }: { searchText: string }) => (

{t("users.noResults.title")}

{searchText ? t("users.noResults.withFilter", { filter: searchText }) : t("users.noResults.default")}

); return (

{t("users.title")}

{t("users.description")}

{loading ? ( ) : users.filter((user) => !user.deleted).length > 0 ? ( !user.deleted) .map((user) => ({ key: user.id, ...user, balance: Number(user.balance), }))} rowKey="id" loading={false} className={TABLE_STYLES} pagination={{ total, pageSize: 20, current: currentPage, onChange: (page) => { setCurrentPage(page); setEditingKey(""); }, showTotal: (total) => ( {t("users.total")} {total} {t("users.totalRecords")} ), }} scroll={{ x: 500 }} onChange={(pagination, filters, sorter) => { if (Array.isArray(sorter)) return; setSortInfo({ field: sorter.columnKey as string, order: sorter.order || null, }); }} /> ) : ( )}
{loading ? ( ) : users.filter((user) => !user.deleted).length > 0 ? ( users .filter((user) => !user.deleted) .map((user) => ) ) : ( )}
{showBlacklist && (
{loading ? ( ) : blacklistUsers.length > 0 ? (
({ key: user.id, ...user, balance: Number(user.balance), }))} rowKey="id" loading={false} className={TABLE_STYLES} pagination={{ total: blacklistTotal, pageSize: 20, current: blacklistCurrentPage, onChange: (page) => { setBlacklistCurrentPage(page); setEditingKey(""); }, showTotal: (total) => ( {t("users.total")} {total} {t("users.totalRecords")} ), }} /> ) : ( )}
{loading ? ( ) : blacklistUsers.length > 0 ? ( blacklistUsers.map((user) => ( )) ) : ( )}
)} {selectedUser && ( setSelectedUser(null)} t={t} /> )} {userToDelete && ( setUserToDelete(null)} onConfirm={handleDeleteUser} t={t} /> )} ); }