"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 }) => (
);
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}
/>
)}
);
}