update table

This commit is contained in:
2026-04-09 15:36:16 +07:00
parent 430063e913
commit b4a667e764
4 changed files with 159 additions and 102 deletions

View File

@@ -8,6 +8,7 @@ import { Modal } from "@/components/ui/modal";
import { responseUserTable, getUserDto, fullDataUser } from "@/interface/admin";
import {
apiDeleteUser,
apiGetAllRole,
apiGetListUser,
apiRestoreUser,
} from "@/service/adminService";
@@ -17,19 +18,18 @@ import { toast } from "sonner";
export type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
export default function UserTable() {
const [limit, setLimit] = useState<number>(5);
const [limitInput, setLimitInput] = useState<string>("5");
const [page, setPage] = useState<number>(1);
const [limitInput, setLimitInput] = useState<string>("5");
const [selectedRole, setSelectedRole] = useState<string>("");
const [roles, setRoles] = useState<{ id: string; name: string }[]>([]);
const [searchTerm, setSearchTerm] = useState<string>("");
const [authProvider, setAuthProvider] = useState<string>("");
const [createdFrom, setCreatedFrom] = useState<string>("");
const [createdTo, setCreatedTo] = useState<string>("");
const [isDeleted, setIsDeleted] = useState<boolean | undefined>(undefined);
const [selectedUser, setSelectedUser] = useState<fullDataUser | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [roleUser, setRoleUser] = useState<fullDataUser | null>(null);
const [isRoleModalOpen, setIsRoleModalOpen] = useState(false);
@@ -44,6 +44,21 @@ export default function UserTable() {
const [sortBy, setSortBy] = useState<SortColumn | undefined>(undefined);
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
useEffect(() => {
const fetchRoles = async () => {
try {
const res = await apiGetAllRole();
console.log("Danh sách role:", res);
if (res?.status) {
setRoles(res.data);
}
} catch (err) {
console.error("Lỗi lấy danh sách role:", err);
}
};
fetchRoles();
}, []);
console.log("Roles đã fetch:", roles);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedParams({
@@ -64,15 +79,13 @@ export default function UserTable() {
limit: debouncedParams.limit,
search: debouncedParams.search || undefined,
auth_provider: debouncedParams.authProvider || undefined,
created_from: createdFrom || undefined,
created_to: createdTo || undefined,
is_deleted: isDeleted,
sort: sortBy,
order: sortOrder,
role_ids: selectedRole ? [selectedRole] : undefined,
};
const response = await apiGetListUser(payload);
if (response?.status) {
setTableData(response);
}
@@ -82,15 +95,7 @@ export default function UserTable() {
} finally {
setLoading(false);
}
}, [
page,
debouncedParams,
createdFrom,
createdTo,
isDeleted,
sortBy,
sortOrder,
]);
}, [page, debouncedParams, isDeleted, sortBy, sortOrder, selectedRole]);
useEffect(() => {
fetchUsers();
@@ -112,34 +117,39 @@ export default function UserTable() {
setSelectedUser(user);
setIsModalOpen(true);
};
const handleOpenRoleModal = (user: fullDataUser) => {
setRoleUser(user);
setIsRoleModalOpen(true);
};
const handleDelete = async (user: fullDataUser) => {
const confirmMessage = `Bạn có chắc chắn muốn khóa/xóa người dùng ${user.profile?.display_name || user.email}?`;
if (window.confirm(confirmMessage)) {
if (
window.confirm(
`Khóa người dùng ${user.profile?.display_name || user.email}?`,
)
) {
try {
await apiDeleteUser(user.id);
toast.success("Đã khóa người dùng thành công!");
fetchUsers();
} catch (err) {
console.error(err);
toast.error("Đã xảy ra lỗi khi xóa!");
}
}
};
const handleRestore = async (user: fullDataUser) => {
const confirmMessage = `Bạn có chắc chắn muốn khôi phục người dùng ${user.profile?.display_name || user.email}?`;
if (window.confirm(confirmMessage)) {
if (
window.confirm(
`Khôi phục người dùng ${user.profile?.display_name || user.email}?`,
)
) {
try {
await apiRestoreUser(user.id);
toast.success("Khôi phục người dùng thành công!");
fetchUsers();
} catch (err) {
console.error(err);
toast.error("Đã xảy ra lỗi khi khôi phục!");
}
}
@@ -149,9 +159,7 @@ export default function UserTable() {
<PageBreadcrumb pageTitle="Quản lý người dùng" />
<div className="space-y-6">
<ComponentCard title="Bộ lọc tìm kiếm">
{/* Grid Layout cho các Filter giống Swagger */}
<div className="grid grid-cols-1 gap-4 mb-6 md:grid-cols-3 lg:grid-cols-4">
{/* Search */}
<div>
<label className="block mb-2 text-sm font-medium">Search</label>
<input
@@ -168,13 +176,14 @@ export default function UserTable() {
<label className="block mb-2 text-sm font-medium">
Auth Provider
</label>
<input
type="text"
placeholder="google"
<select
value={authProvider}
onChange={(e) => setAuthProvider(e.target.value)}
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700"
/>
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 cursor-pointer bg-white"
>
<option value="">Tất cả</option>
<option value="google">Google</option>
</select>
</div>
<div>
@@ -226,6 +235,9 @@ export default function UserTable() {
sortBy={sortBy}
sortOrder={sortOrder}
onViewDetail={handleOpenDetail}
roles={roles}
selectedRole={selectedRole}
onFilterRole={(role) => setSelectedRole(role)}
/>
</div>

View File

@@ -13,22 +13,32 @@ import { fullDataUser } from "@/interface/admin";
type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
interface Role {
id: string;
name: string;
}
interface BasicTableOneProps {
data: fullDataUser[];
onSort: (column: SortColumn) => void;
onViewDetail: (user: fullDataUser) => void;
sortBy?: SortColumn;
sortOrder?: "asc" | "desc";
onFilterRole?: (role: string) => void;
selectedRole?: string;
roles?: Role[];
}
export default function BasicTableOne({
data,
data,
onSort,
onViewDetail,
sortBy,
sortOrder,
onFilterRole,
selectedRole,
roles = [],
}: BasicTableOneProps) {
const formatDate = (dateString: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
@@ -48,7 +58,6 @@ export default function BasicTableOne({
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
@@ -62,7 +71,6 @@ export default function BasicTableOne({
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
@@ -84,27 +92,58 @@ export default function BasicTableOne({
<TableRow>
<TableCell
isHeader
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 min-w-[265px] max-w-[265px]"
>
<div
className="flex items-center cursor-pointer select-none"
onClick={() => onSort("display_name")}
>
Người dùng
<SortIcon column="display_name" />
Người dùng <SortIcon column="display_name" />
</div>
</TableCell>
<TableCell
isHeader
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 min-w-[337px] max-w-[337px]"
>
<div
className="flex items-center cursor-pointer select-none"
onClick={() => onSort("email")}
>
Email
<SortIcon column="email" />
Email <SortIcon column="email" />
</div>
</TableCell>
<TableCell
isHeader
className="px-5 py-3 font-medium text-start text-theme-xs min-w-[188px] max-w-[188px]"
>
<div className="relative inline-flex items-center group">
<select
value={selectedRole}
onChange={(e) => onFilterRole?.(e.target.value)}
className="bg-transparent border-none outline-none cursor-pointer appearance-none text-gray-500 dark:text-gray-400 pr-5 hover:text-brand-500 transition-colors font-medium"
>
<option value="">Vai trò (Tất cả)</option>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))}
</select>
<svg
className="w-3 h-3 absolute right-0 text-gray-400 pointer-events-none group-hover:text-brand-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</TableCell>
@@ -112,11 +151,6 @@ export default function BasicTableOne({
isHeader
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
>
Vai trò
</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
Trạng thái
</TableCell>
@@ -128,8 +162,7 @@ export default function BasicTableOne({
className="flex items-center cursor-pointer select-none"
onClick={() => onSort("created_at")}
>
Ngày tham gia
<SortIcon column="created_at" />
Ngày tham gia <SortIcon column="created_at" />
</div>
</TableCell>
@@ -141,11 +174,14 @@ export default function BasicTableOne({
className="flex items-center cursor-pointer select-none"
onClick={() => onSort("updated_at")}
>
Cập nhật
<SortIcon column="updated_at" />
Cập nhật <SortIcon column="updated_at" />
</div>
</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-center text-theme-xs dark:text-gray-400">
<TableCell
isHeader
className="px-5 py-3 font-medium text-gray-500 text-center text-theme-xs dark:text-gray-400"
>
Thao tác
</TableCell>
</TableRow>
@@ -166,7 +202,7 @@ export default function BasicTableOne({
width={40}
height={40}
src={user.profile.avatar_url}
alt={user.profile.display_name || "Avatar"}
alt="Avatar"
className="object-cover w-full h-full"
/>
) : (
@@ -192,17 +228,17 @@ export default function BasicTableOne({
<TableCell className="px-5 py-4 text-start text-theme-sm">
<div className="flex flex-wrap gap-1">
{user.roles && user.roles.length > 0 ? (
user.roles.map((role) => (
<span
key={role.id}
className="px-2 py-0.5 rounded-md bg-brand-50 text-brand-600 dark:bg-brand-500/10 dark:text-brand-400 text-[10px] font-normal uppercase tracking-wider"
>
{role.name}
</span>
))
) : (
<span className="text-gray-400 italic">No Role</span>
{user.roles?.map((role) => (
<span
key={role.id}
className="px-2 py-0.5 rounded-md bg-brand-50 text-brand-600 dark:bg-brand-500/10 dark:text-brand-400 text-[10px] uppercase"
>
{role.name}
</span>
)) || (
<span className="text-gray-400 italic text-[10px]">
No Role
</span>
)}
</div>
</TableCell>
@@ -220,24 +256,32 @@ export default function BasicTableOne({
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
{formatDate(user.created_at)}
</TableCell>
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
{formatDate(user.updated_at)}
</TableCell>
<TableCell className="px-5 py-4 text-center">
<button
onClick={() => onViewDetail(user)}
className="text-brand-500 hover:text-brand-600 font-medium text-theme-sm transition-colors"
>
Chi tiết
</button>
</TableCell>
<button
onClick={() => onViewDetail(user)}
className="text-brand-500 hover:text-brand-600 font-medium text-theme-sm"
>
Chi tiết
</button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell className="px-5 py-4 text-center text-gray-500 italic"> </TableCell>
<TableCell
colSpan={7}
className="px-5 py-24 text-center text-gray-500 italic"
>
<div className="flex flex-col items-center justify-center gap-2">
<p className="text-theme-sm">
Không tìm thấy dữ liệu người dùng
</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>

View File

@@ -1,54 +1,49 @@
import React, { ReactNode } from "react";
import React, { ReactNode, TdHTMLAttributes, HTMLAttributes } from "react";
// Props for Table
interface TableProps {
children: ReactNode; // Table content (thead, tbody, etc.)
className?: string; // Optional className for styling
interface TableProps extends HTMLAttributes<HTMLTableElement> {
children: ReactNode;
}
// Props for TableHeader
interface TableHeaderProps {
children: ReactNode; // Header row(s)
className?: string; // Optional className for styling
interface TableHeaderProps extends HTMLAttributes<HTMLTableSectionElement> {
children: ReactNode;
}
// Props for TableBody
interface TableBodyProps {
children: ReactNode; // Body row(s)
className?: string; // Optional className for styling
interface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {
children: ReactNode;
}
// Props for TableRow
interface TableRowProps {
children: ReactNode; // Cells (th or td)
className?: string; // Optional className for styling
interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {
children: ReactNode;
}
// Props for TableCell
interface TableCellProps {
children: ReactNode; // Cell content
isHeader?: boolean; // If true, renders as <th>, otherwise <td>
className?: string; // Optional className for styling
// Props for TableCell - Hỗ trợ colSpan, rowSpan...
interface TableCellProps extends TdHTMLAttributes<HTMLTableCellElement> {
children: ReactNode;
isHeader?: boolean;
}
// Table Component
const Table: React.FC<TableProps> = ({ children, className }) => {
return <table className={`min-w-full ${className}`}>{children}</table>;
const Table: React.FC<TableProps> = ({ children, className, ...props }) => {
return <table className={`min-w-full ${className}`} {...props}>{children}</table>;
};
// TableHeader Component
const TableHeader: React.FC<TableHeaderProps> = ({ children, className }) => {
return <thead className={className}>{children}</thead>;
const TableHeader: React.FC<TableHeaderProps> = ({ children, className, ...props }) => {
return <thead className={className} {...props}>{children}</thead>;
};
// TableBody Component
const TableBody: React.FC<TableBodyProps> = ({ children, className }) => {
return <tbody className={className}>{children}</tbody>;
const TableBody: React.FC<TableBodyProps> = ({ children, className, ...props }) => {
return <tbody className={className} {...props}>{children}</tbody>;
};
// TableRow Component
const TableRow: React.FC<TableRowProps> = ({ children, className }) => {
return <tr className={className}>{children}</tr>;
const TableRow: React.FC<TableRowProps> = ({ children, className, ...props }) => {
return <tr className={className} {...props}>{children}</tr>;
};
// TableCell Component
@@ -56,9 +51,14 @@ const TableCell: React.FC<TableCellProps> = ({
children,
isHeader = false,
className,
...props
}) => {
const CellTag = isHeader ? "th" : "td";
return <CellTag className={` ${className}`}>{children}</CellTag>;
return (
<CellTag className={className} {...(props as any)}>
{children}
</CellTag>
);
};
export { Table, TableHeader, TableBody, TableRow, TableCell };

View File

@@ -22,6 +22,7 @@ export const apiRestoreUser = async (id: string) => {
const response = await api.patch(API.Admin.RESTORE_USER(id));
return response?.data;
};
export const apiGetAllRole = async () => {
const response = await api.get(API.Admin.GET_ALL_ROLE);
return response?.data;